2026-02-02 12:24:50 -08:00
|
|
|
|
#include "ui/game_screen.hpp"
|
2026-02-06 14:24:38 -08:00
|
|
|
|
#include "rendering/character_preview.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-02-04 17:37:28 -08:00
|
|
|
|
#include "core/coordinates.hpp"
|
2026-02-04 18:27:52 -08:00
|
|
|
|
#include "core/spawn_presets.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-06 14:30:54 -08:00
|
|
|
|
#include "pipeline/blp_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>
|
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-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) {
|
2026-03-13 10:30:54 -07:00
|
|
|
|
static const char* kQualHex[] = {"9d9d9d","ffffff","1eff00","0070dd","a335ee","ff8000","e6cc80","e6cc80"};
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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).
|
|
|
|
|
|
void renderCoinsText(uint32_t g, uint32_t s, uint32_t c) {
|
|
|
|
|
|
bool any = false;
|
|
|
|
|
|
if (g > 0) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.00f, 0.82f, 0.00f, 1.0f), "%ug", g);
|
|
|
|
|
|
any = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (s > 0 || g > 0) {
|
|
|
|
|
|
if (any) ImGui::SameLine(0, 3);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.80f, 0.80f, 0.80f, 1.0f), "%us", s);
|
|
|
|
|
|
any = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (any) ImGui::SameLine(0, 3);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.72f, 0.45f, 0.20f, 1.0f), "%uc", c);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 08:33:34 -07:00
|
|
|
|
// Return the canonical Blizzard class color as ImVec4.
|
|
|
|
|
|
// classId is byte 1 of UNIT_FIELD_BYTES_0 (or CharacterData::classId).
|
|
|
|
|
|
// Returns a neutral light-gray for unknown / class 0.
|
|
|
|
|
|
ImVec4 classColorVec4(uint8_t classId) {
|
|
|
|
|
|
switch (classId) {
|
|
|
|
|
|
case 1: return ImVec4(0.78f, 0.61f, 0.43f, 1.0f); // Warrior #C79C6E
|
|
|
|
|
|
case 2: return ImVec4(0.96f, 0.55f, 0.73f, 1.0f); // Paladin #F58CBA
|
|
|
|
|
|
case 3: return ImVec4(0.67f, 0.83f, 0.45f, 1.0f); // Hunter #ABD473
|
|
|
|
|
|
case 4: return ImVec4(1.00f, 0.96f, 0.41f, 1.0f); // Rogue #FFF569
|
|
|
|
|
|
case 5: return ImVec4(1.00f, 1.00f, 1.00f, 1.0f); // Priest #FFFFFF
|
|
|
|
|
|
case 6: return ImVec4(0.77f, 0.12f, 0.23f, 1.0f); // DeathKnight #C41F3B
|
|
|
|
|
|
case 7: return ImVec4(0.00f, 0.44f, 0.87f, 1.0f); // Shaman #0070DE
|
|
|
|
|
|
case 8: return ImVec4(0.41f, 0.80f, 0.94f, 1.0f); // Mage #69CCF0
|
|
|
|
|
|
case 9: return ImVec4(0.58f, 0.51f, 0.79f, 1.0f); // Warlock #9482C9
|
|
|
|
|
|
case 11: return ImVec4(1.00f, 0.49f, 0.04f, 1.0f); // Druid #FF7D0A
|
|
|
|
|
|
default: return ImVec4(0.85f, 0.85f, 0.85f, 1.0f); // unknown
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ImU32 variant with alpha in [0,255].
|
|
|
|
|
|
ImU32 classColorU32(uint8_t classId, int alpha = 255) {
|
|
|
|
|
|
ImVec4 c = classColorVec4(classId);
|
|
|
|
|
|
return IM_COL32(static_cast<int>(c.x * 255), static_cast<int>(c.y * 255),
|
|
|
|
|
|
static_cast<int>(c.z * 255), alpha);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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-12 08:46:26 -07:00
|
|
|
|
// Return the English class name for a class ID (1-11), or "Unknown".
|
|
|
|
|
|
const char* classNameStr(uint8_t classId) {
|
|
|
|
|
|
static const char* kNames[] = {
|
|
|
|
|
|
"Unknown","Warrior","Paladin","Hunter","Rogue","Priest",
|
|
|
|
|
|
"Death Knight","Shaman","Mage","Warlock","","Druid"
|
|
|
|
|
|
};
|
|
|
|
|
|
return (classId < 12) ? kNames[classId] : "Unknown";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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-14 14:30:09 -08:00
|
|
|
|
initChatTabs();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::initChatTabs() {
|
|
|
|
|
|
chatTabs_.clear();
|
|
|
|
|
|
// General tab: shows everything
|
2026-03-12 08:54:29 -07:00
|
|
|
|
chatTabs_.push_back({"General", ~0ULL});
|
2026-03-11 22:13:22 -07:00
|
|
|
|
// Combat tab: system, loot, skills, achievements, and NPC speech/emotes
|
2026-03-12 08:54:29 -07:00
|
|
|
|
chatTabs_.push_back({"Combat", (1ULL << static_cast<uint8_t>(game::ChatType::SYSTEM)) |
|
|
|
|
|
|
(1ULL << static_cast<uint8_t>(game::ChatType::LOOT)) |
|
|
|
|
|
|
(1ULL << static_cast<uint8_t>(game::ChatType::SKILL)) |
|
|
|
|
|
|
(1ULL << static_cast<uint8_t>(game::ChatType::ACHIEVEMENT)) |
|
|
|
|
|
|
(1ULL << static_cast<uint8_t>(game::ChatType::GUILD_ACHIEVEMENT)) |
|
|
|
|
|
|
(1ULL << static_cast<uint8_t>(game::ChatType::MONSTER_SAY)) |
|
|
|
|
|
|
(1ULL << static_cast<uint8_t>(game::ChatType::MONSTER_YELL)) |
|
|
|
|
|
|
(1ULL << static_cast<uint8_t>(game::ChatType::MONSTER_EMOTE)) |
|
|
|
|
|
|
(1ULL << static_cast<uint8_t>(game::ChatType::MONSTER_WHISPER)) |
|
|
|
|
|
|
(1ULL << static_cast<uint8_t>(game::ChatType::MONSTER_PARTY)) |
|
|
|
|
|
|
(1ULL << static_cast<uint8_t>(game::ChatType::RAID_BOSS_WHISPER)) |
|
|
|
|
|
|
(1ULL << static_cast<uint8_t>(game::ChatType::RAID_BOSS_EMOTE))});
|
2026-02-14 14:30:09 -08:00
|
|
|
|
// Whispers tab
|
2026-03-12 08:54:29 -07:00
|
|
|
|
chatTabs_.push_back({"Whispers", (1ULL << static_cast<uint8_t>(game::ChatType::WHISPER)) |
|
|
|
|
|
|
(1ULL << static_cast<uint8_t>(game::ChatType::WHISPER_INFORM))});
|
2026-03-12 11:21:12 -07:00
|
|
|
|
// Guild tab: guild and officer chat
|
|
|
|
|
|
chatTabs_.push_back({"Guild", (1ULL << static_cast<uint8_t>(game::ChatType::GUILD)) |
|
|
|
|
|
|
(1ULL << static_cast<uint8_t>(game::ChatType::OFFICER)) |
|
|
|
|
|
|
(1ULL << static_cast<uint8_t>(game::ChatType::GUILD_ACHIEVEMENT))});
|
2026-02-14 14:30:09 -08:00
|
|
|
|
// Trade/LFG tab: channel messages
|
2026-03-12 08:54:29 -07:00
|
|
|
|
chatTabs_.push_back({"Trade/LFG", (1ULL << static_cast<uint8_t>(game::ChatType::CHANNEL))});
|
2026-03-12 11:23:01 -07:00
|
|
|
|
// Reset unread counts to match new tab list
|
|
|
|
|
|
chatTabUnread_.assign(chatTabs_.size(), 0);
|
|
|
|
|
|
chatTabSeenCount_ = 0;
|
2026-02-14 14:30:09 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool GameScreen::shouldShowMessage(const game::MessageChatData& msg, int tabIndex) const {
|
|
|
|
|
|
if (tabIndex < 0 || tabIndex >= static_cast<int>(chatTabs_.size())) return true;
|
|
|
|
|
|
const auto& tab = chatTabs_[tabIndex];
|
2026-03-12 08:54:29 -07:00
|
|
|
|
if (tab.typeMask == ~0ULL) return true; // General tab shows all
|
2026-02-14 14:30:09 -08:00
|
|
|
|
|
2026-03-12 08:54:29 -07:00
|
|
|
|
uint64_t typeBit = 1ULL << static_cast<uint8_t>(msg.type);
|
2026-02-14 14:30:09 -08:00
|
|
|
|
|
2026-03-12 11:21:12 -07:00
|
|
|
|
// For Trade/LFG tab (now index 4), also filter by channel name
|
|
|
|
|
|
if (tabIndex == 4 && msg.type == game::ChatType::CHANNEL) {
|
2026-02-14 14:30:09 -08:00
|
|
|
|
const std::string& ch = msg.channelName;
|
|
|
|
|
|
if (ch.find("Trade") == std::string::npos &&
|
|
|
|
|
|
ch.find("General") == std::string::npos &&
|
2026-02-16 21:16:25 -08:00
|
|
|
|
ch.find("LookingForGroup") == std::string::npos &&
|
|
|
|
|
|
ch.find("Local") == std::string::npos) {
|
2026-02-14 14:30:09 -08:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (tab.typeMask & typeBit) != 0;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::render(game::GameHandler& gameHandler) {
|
2026-02-14 14:30:09 -08:00
|
|
|
|
// Set up chat bubble callback (once)
|
|
|
|
|
|
if (!chatBubbleCallbackSet_) {
|
|
|
|
|
|
gameHandler.setChatBubbleCallback([this](uint64_t guid, const std::string& msg, bool isYell) {
|
|
|
|
|
|
float duration = 8.0f + static_cast<float>(msg.size()) * 0.06f;
|
|
|
|
|
|
if (isYell) duration += 2.0f;
|
|
|
|
|
|
if (duration > 15.0f) duration = 15.0f;
|
|
|
|
|
|
|
|
|
|
|
|
// Replace existing bubble for same sender
|
|
|
|
|
|
for (auto& b : chatBubbles_) {
|
|
|
|
|
|
if (b.senderGuid == guid) {
|
|
|
|
|
|
b.message = msg;
|
|
|
|
|
|
b.timeRemaining = duration;
|
|
|
|
|
|
b.totalDuration = duration;
|
|
|
|
|
|
b.isYell = isYell;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Evict oldest if too many
|
|
|
|
|
|
if (chatBubbles_.size() >= 10) {
|
|
|
|
|
|
chatBubbles_.erase(chatBubbles_.begin());
|
|
|
|
|
|
}
|
|
|
|
|
|
chatBubbles_.push_back({guid, msg, duration, duration, isYell});
|
|
|
|
|
|
});
|
|
|
|
|
|
chatBubbleCallbackSet_ = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 23:10:21 -07:00
|
|
|
|
// Set up level-up callback (once)
|
|
|
|
|
|
if (!levelUpCallbackSet_) {
|
2026-03-12 17:54:49 -07:00
|
|
|
|
gameHandler.setLevelUpCallback([this, &gameHandler](uint32_t newLevel) {
|
2026-03-11 23:10:21 -07:00
|
|
|
|
levelUpFlashAlpha_ = 1.0f;
|
|
|
|
|
|
levelUpDisplayLevel_ = newLevel;
|
2026-03-12 17:54:49 -07:00
|
|
|
|
const auto& d = gameHandler.getLastLevelUpDeltas();
|
|
|
|
|
|
triggerDing(newLevel, d.hp, d.mana, d.str, d.agi, d.sta, d.intel, d.spi);
|
2026-03-11 23:10:21 -07:00
|
|
|
|
});
|
|
|
|
|
|
levelUpCallbackSet_ = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 00:56:31 -07:00
|
|
|
|
// Set up achievement toast callback (once)
|
|
|
|
|
|
if (!achievementCallbackSet_) {
|
|
|
|
|
|
gameHandler.setAchievementEarnedCallback([this](uint32_t id, const std::string& name) {
|
|
|
|
|
|
triggerAchievementToast(id, name);
|
|
|
|
|
|
});
|
|
|
|
|
|
achievementCallbackSet_ = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 15:42:55 -07:00
|
|
|
|
// Set up area discovery toast callback (once)
|
|
|
|
|
|
if (!areaDiscoveryCallbackSet_) {
|
|
|
|
|
|
gameHandler.setAreaDiscoveryCallback([this](const std::string& areaName, uint32_t xpGained) {
|
|
|
|
|
|
discoveryToastName_ = areaName.empty() ? "New Area" : areaName;
|
|
|
|
|
|
discoveryToastXP_ = xpGained;
|
|
|
|
|
|
discoveryToastTimer_ = DISCOVERY_TOAST_DURATION;
|
|
|
|
|
|
});
|
|
|
|
|
|
areaDiscoveryCallbackSet_ = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 15:57:09 -07:00
|
|
|
|
// Set up quest objective progress toast callback (once)
|
|
|
|
|
|
if (!questProgressCallbackSet_) {
|
|
|
|
|
|
gameHandler.setQuestProgressCallback([this](const std::string& questTitle,
|
|
|
|
|
|
const std::string& objectiveName,
|
|
|
|
|
|
uint32_t current, uint32_t required) {
|
|
|
|
|
|
// Coalesce: if the same objective already has a toast, just update counts
|
|
|
|
|
|
for (auto& t : questToasts_) {
|
|
|
|
|
|
if (t.questTitle == questTitle && t.objectiveName == objectiveName) {
|
|
|
|
|
|
t.current = current;
|
|
|
|
|
|
t.required = required;
|
|
|
|
|
|
t.age = 0.0f; // restart lifetime
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (questToasts_.size() >= 4) questToasts_.erase(questToasts_.begin());
|
|
|
|
|
|
questToasts_.push_back({questTitle, objectiveName, current, required, 0.0f});
|
|
|
|
|
|
});
|
|
|
|
|
|
questProgressCallbackSet_ = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 16:12:21 -07:00
|
|
|
|
// Set up other-player level-up toast callback (once)
|
|
|
|
|
|
if (!otherPlayerLevelUpCallbackSet_) {
|
|
|
|
|
|
gameHandler.setOtherPlayerLevelUpCallback([this](uint64_t guid, uint32_t newLevel) {
|
|
|
|
|
|
// Coalesce: update existing toast for same player
|
|
|
|
|
|
for (auto& t : playerLevelUpToasts_) {
|
|
|
|
|
|
if (t.guid == guid) {
|
|
|
|
|
|
t.newLevel = newLevel;
|
|
|
|
|
|
t.age = 0.0f;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (playerLevelUpToasts_.size() >= 3)
|
|
|
|
|
|
playerLevelUpToasts_.erase(playerLevelUpToasts_.begin());
|
|
|
|
|
|
playerLevelUpToasts_.push_back({guid, "", newLevel, 0.0f});
|
|
|
|
|
|
});
|
|
|
|
|
|
otherPlayerLevelUpCallbackSet_ = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 16:19:25 -07:00
|
|
|
|
// Set up PvP honor credit toast callback (once)
|
|
|
|
|
|
if (!pvpHonorCallbackSet_) {
|
|
|
|
|
|
gameHandler.setPvpHonorCallback([this](uint32_t honor, uint64_t /*victimGuid*/, uint32_t rank) {
|
|
|
|
|
|
if (honor == 0) return;
|
|
|
|
|
|
pvpHonorToasts_.push_back({honor, rank, 0.0f});
|
|
|
|
|
|
if (pvpHonorToasts_.size() > 4)
|
|
|
|
|
|
pvpHonorToasts_.erase(pvpHonorToasts_.begin());
|
|
|
|
|
|
});
|
|
|
|
|
|
pvpHonorCallbackSet_ = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 16:24:11 -07:00
|
|
|
|
// Set up item loot toast callback (once)
|
|
|
|
|
|
if (!itemLootCallbackSet_) {
|
|
|
|
|
|
gameHandler.setItemLootCallback([this](uint32_t itemId, uint32_t count,
|
|
|
|
|
|
uint32_t quality, const std::string& name) {
|
|
|
|
|
|
// Coalesce: if same item already in queue, bump count and reset age
|
|
|
|
|
|
for (auto& t : itemLootToasts_) {
|
|
|
|
|
|
if (t.itemId == itemId) {
|
|
|
|
|
|
t.count += count;
|
|
|
|
|
|
t.age = 0.0f;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (itemLootToasts_.size() >= 5)
|
|
|
|
|
|
itemLootToasts_.erase(itemLootToasts_.begin());
|
|
|
|
|
|
itemLootToasts_.push_back({itemId, count, quality, name, 0.0f});
|
|
|
|
|
|
});
|
|
|
|
|
|
itemLootCallbackSet_ = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 16:33:08 -07:00
|
|
|
|
// Set up ghost-state callback to flash "You have been resurrected!" on revival (once)
|
|
|
|
|
|
if (!ghostStateCallbackSet_) {
|
|
|
|
|
|
gameHandler.setGhostStateCallback([this](bool isGhost) {
|
|
|
|
|
|
if (!isGhost) {
|
|
|
|
|
|
// Transitioning ghost→alive: trigger the resurrection flash
|
|
|
|
|
|
resurrectFlashTimer_ = kResurrectFlashDuration;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
ghostStateCallbackSet_ = 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-12 01:51:18 -07:00
|
|
|
|
// Set up reputation change toast callback (once)
|
|
|
|
|
|
if (!repChangeCallbackSet_) {
|
|
|
|
|
|
gameHandler.setRepChangeCallback([this](const std::string& name, int32_t delta, int32_t standing) {
|
|
|
|
|
|
repToasts_.push_back({name, delta, standing, 0.0f});
|
|
|
|
|
|
if (repToasts_.size() > 4) repToasts_.erase(repToasts_.begin());
|
|
|
|
|
|
});
|
|
|
|
|
|
repChangeCallbackSet_ = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 04:53:03 -07:00
|
|
|
|
// Set up quest completion toast callback (once)
|
|
|
|
|
|
if (!questCompleteCallbackSet_) {
|
|
|
|
|
|
gameHandler.setQuestCompleteCallback([this](uint32_t id, const std::string& title) {
|
|
|
|
|
|
questCompleteToasts_.push_back({id, title, 0.0f});
|
|
|
|
|
|
if (questCompleteToasts_.size() > 3) questCompleteToasts_.erase(questCompleteToasts_.begin());
|
|
|
|
|
|
});
|
|
|
|
|
|
questCompleteCallbackSet_ = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 20:19:39 -08:00
|
|
|
|
// Apply UI transparency setting
|
|
|
|
|
|
float prevAlpha = ImGui::GetStyle().Alpha;
|
|
|
|
|
|
ImGui::GetStyle().Alpha = uiOpacity_;
|
|
|
|
|
|
|
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()) {
|
|
|
|
|
|
minimap->setOpacity(uiOpacity_);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-17 16:26:49 -08:00
|
|
|
|
// Apply initial settings when renderer becomes available
|
2026-02-09 17:39:21 -08:00
|
|
|
|
if (!minimapSettingsApplied_) {
|
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
if (auto* minimap = renderer->getMinimap()) {
|
2026-02-11 17:30:57 -08:00
|
|
|
|
minimapRotate_ = false;
|
|
|
|
|
|
pendingMinimapRotate = false;
|
|
|
|
|
|
minimap->setRotateWithCamera(false);
|
2026-02-09 17:39:21 -08:00
|
|
|
|
minimap->setSquareShape(minimapSquare_);
|
|
|
|
|
|
minimapSettingsApplied_ = true;
|
|
|
|
|
|
}
|
2026-02-17 16:26:49 -08:00
|
|
|
|
if (auto* zm = renderer->getZoneManager()) {
|
|
|
|
|
|
zm->setUseOriginalSoundtrack(pendingUseOriginalSoundtrack);
|
|
|
|
|
|
}
|
2026-02-21 01:26:16 -08:00
|
|
|
|
if (auto* tm = renderer->getTerrainManager()) {
|
|
|
|
|
|
tm->setGroundClutterDensityScale(static_cast<float>(pendingGroundClutterDensity) / 100.0f);
|
|
|
|
|
|
}
|
2026-02-17 16:26:49 -08:00
|
|
|
|
// Restore mute state: save actual master volume first, then apply mute
|
|
|
|
|
|
if (soundMuted_) {
|
|
|
|
|
|
float actual = audio::AudioEngine::instance().getMasterVolume();
|
|
|
|
|
|
preMuteVolume_ = (actual > 0.0f) ? actual
|
|
|
|
|
|
: static_cast<float>(pendingMasterVolume) / 100.0f;
|
|
|
|
|
|
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
|
|
|
|
|
|
if (!volumeSettingsApplied_) {
|
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
if (renderer && renderer->getUiSoundManager()) {
|
2026-02-19 02:46:52 -08:00
|
|
|
|
float masterScale = soundMuted_ ? 0.0f : static_cast<float>(pendingMasterVolume) / 100.0f;
|
2026-02-23 07:51:10 -08:00
|
|
|
|
audio::AudioEngine::instance().setMasterVolume(masterScale);
|
2026-02-17 17:37:20 -08:00
|
|
|
|
if (auto* music = renderer->getMusicManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
music->setVolume(pendingMusicVolume);
|
2026-02-17 17:37:20 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* ambient = renderer->getAmbientSoundManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
ambient->setVolumeScale(pendingAmbientVolume / 100.0f);
|
2026-02-17 17:37:20 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* ui = renderer->getUiSoundManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
ui->setVolumeScale(pendingUiVolume / 100.0f);
|
2026-02-17 17:37:20 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* combat = renderer->getCombatSoundManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
combat->setVolumeScale(pendingCombatVolume / 100.0f);
|
2026-02-17 17:37:20 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* spell = renderer->getSpellSoundManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
spell->setVolumeScale(pendingSpellVolume / 100.0f);
|
2026-02-17 17:37:20 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* movement = renderer->getMovementSoundManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
movement->setVolumeScale(pendingMovementVolume / 100.0f);
|
2026-02-17 17:37:20 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* footstep = renderer->getFootstepManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
footstep->setVolumeScale(pendingFootstepVolume / 100.0f);
|
2026-02-17 17:37:20 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* npcVoice = renderer->getNpcVoiceManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
npcVoice->setVolumeScale(pendingNpcVoiceVolume / 100.0f);
|
2026-02-17 17:37:20 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* mount = renderer->getMountSoundManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
mount->setVolumeScale(pendingMountVolume / 100.0f);
|
2026-02-17 17:37:20 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* activity = renderer->getActivitySoundManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
activity->setVolumeScale(pendingActivityVolume / 100.0f);
|
2026-02-17 17:37:20 -08:00
|
|
|
|
}
|
|
|
|
|
|
volumeSettingsApplied_ = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 02:59:24 -08:00
|
|
|
|
// Apply saved MSAA setting once when renderer is available
|
|
|
|
|
|
if (!msaaSettingsApplied_ && pendingAntiAliasing > 0) {
|
|
|
|
|
|
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
|
|
|
|
|
|
};
|
|
|
|
|
|
renderer->setMsaaSamples(aaSamples[pendingAntiAliasing]);
|
|
|
|
|
|
msaaSettingsApplied_ = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
msaaSettingsApplied_ = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 16:43:48 -07:00
|
|
|
|
// Apply saved FXAA setting once when renderer is available
|
|
|
|
|
|
if (!fxaaSettingsApplied_) {
|
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
renderer->setFXAAEnabled(pendingFXAA);
|
|
|
|
|
|
fxaaSettingsApplied_ = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-06 19:15:34 -08:00
|
|
|
|
// Apply saved water refraction setting once when renderer is available
|
|
|
|
|
|
if (!waterRefractionApplied_) {
|
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
renderer->setWaterRefractionEnabled(pendingWaterRefraction);
|
|
|
|
|
|
waterRefractionApplied_ = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-23 01:10:58 -08:00
|
|
|
|
// Apply saved normal mapping / POM settings once when WMO renderer is available
|
|
|
|
|
|
if (!normalMapSettingsApplied_) {
|
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
if (auto* wr = renderer->getWMORenderer()) {
|
|
|
|
|
|
wr->setNormalMappingEnabled(pendingNormalMapping);
|
2026-02-23 01:18:42 -08:00
|
|
|
|
wr->setNormalMapStrength(pendingNormalMapStrength);
|
2026-02-23 01:10:58 -08:00
|
|
|
|
wr->setPOMEnabled(pendingPOM);
|
|
|
|
|
|
wr->setPOMQuality(pendingPOMQuality);
|
2026-02-23 01:40:23 -08:00
|
|
|
|
if (auto* cr = renderer->getCharacterRenderer()) {
|
|
|
|
|
|
cr->setNormalMappingEnabled(pendingNormalMapping);
|
|
|
|
|
|
cr->setNormalMapStrength(pendingNormalMapStrength);
|
|
|
|
|
|
cr->setPOMEnabled(pendingPOM);
|
|
|
|
|
|
cr->setPOMQuality(pendingPOMQuality);
|
|
|
|
|
|
}
|
2026-02-23 01:10:58 -08:00
|
|
|
|
normalMapSettingsApplied_ = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-08 20:22:11 -07:00
|
|
|
|
// Apply saved upscaling setting once when renderer is available
|
|
|
|
|
|
if (!fsrSettingsApplied_) {
|
2026-03-07 22:03:28 -08:00
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
if (renderer) {
|
2026-03-08 21:17:04 -07:00
|
|
|
|
static const float fsrScales[] = { 0.77f, 0.67f, 0.59f, 1.00f };
|
|
|
|
|
|
pendingFSRQuality = std::clamp(pendingFSRQuality, 0, 3);
|
2026-03-07 22:03:28 -08:00
|
|
|
|
renderer->setFSRQuality(fsrScales[pendingFSRQuality]);
|
|
|
|
|
|
renderer->setFSRSharpness(pendingFSRSharpness);
|
2026-03-08 20:56:22 -07:00
|
|
|
|
renderer->setFSR2DebugTuning(pendingFSR2JitterSign, pendingFSR2MotionVecScaleX, pendingFSR2MotionVecScaleY);
|
2026-03-08 22:53:21 -07:00
|
|
|
|
renderer->setAmdFsr3FramegenEnabled(pendingAMDFramegen);
|
2026-03-08 20:48:46 -07:00
|
|
|
|
// Safety fallback: persisted FSR2 can still hang on some systems during startup.
|
|
|
|
|
|
// Require explicit opt-in for startup FSR2; otherwise fall back to FSR1.
|
|
|
|
|
|
const bool allowStartupFsr2 = (std::getenv("WOWEE_ALLOW_STARTUP_FSR2") != nullptr);
|
|
|
|
|
|
int effectiveMode = pendingUpscalingMode;
|
|
|
|
|
|
if (effectiveMode == 2 && !allowStartupFsr2) {
|
|
|
|
|
|
static bool warnedStartupFsr2Fallback = false;
|
|
|
|
|
|
if (!warnedStartupFsr2Fallback) {
|
|
|
|
|
|
LOG_WARNING("Startup FSR2 is disabled by default for stability; falling back to FSR1. Set WOWEE_ALLOW_STARTUP_FSR2=1 to override.");
|
|
|
|
|
|
warnedStartupFsr2Fallback = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
effectiveMode = 1;
|
|
|
|
|
|
pendingUpscalingMode = 1;
|
|
|
|
|
|
pendingFSR = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If explicitly enabled, still defer FSR2 until fully in-world.
|
|
|
|
|
|
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-08 20:45:26 -07:00
|
|
|
|
fsrSettingsApplied_ = true;
|
|
|
|
|
|
}
|
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-02-17 16:31:00 -08:00
|
|
|
|
gameHandler.setAutoLoot(pendingAutoLoot);
|
2026-03-17 20:21:06 -07:00
|
|
|
|
gameHandler.setAutoSellGrey(pendingAutoSellGrey);
|
2026-03-17 20:27:45 -07:00
|
|
|
|
gameHandler.setAutoRepair(pendingAutoRepair);
|
2026-02-17 16:31:00 -08:00
|
|
|
|
|
2026-03-12 05:44:25 -07:00
|
|
|
|
// Zone entry detection — fire a toast when the renderer's zone name changes
|
|
|
|
|
|
if (auto* rend = core::Application::getInstance().getRenderer()) {
|
|
|
|
|
|
const std::string& curZone = rend->getCurrentZoneName();
|
|
|
|
|
|
if (!curZone.empty() && curZone != lastKnownZone_) {
|
|
|
|
|
|
if (!lastKnownZone_.empty()) {
|
|
|
|
|
|
// Genuine zone change (not first entry)
|
|
|
|
|
|
zoneToasts_.push_back({curZone, 0.0f});
|
|
|
|
|
|
if (zoneToasts_.size() > 3)
|
|
|
|
|
|
zoneToasts_.erase(zoneToasts_.begin());
|
|
|
|
|
|
}
|
|
|
|
|
|
lastKnownZone_ = curZone;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 18:27:59 -08:00
|
|
|
|
// Sync chat auto-join settings to GameHandler
|
|
|
|
|
|
gameHandler.chatAutoJoin.general = chatAutoJoinGeneral_;
|
|
|
|
|
|
gameHandler.chatAutoJoin.trade = chatAutoJoinTrade_;
|
|
|
|
|
|
gameHandler.chatAutoJoin.localDefense = chatAutoJoinLocalDefense_;
|
|
|
|
|
|
gameHandler.chatAutoJoin.lfg = chatAutoJoinLFG_;
|
2026-02-16 21:16:25 -08:00
|
|
|
|
gameHandler.chatAutoJoin.local = 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) {
|
|
|
|
|
|
renderChatWindow(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
|
|
|
|
// ---- 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-12 01:51:18 -07:00
|
|
|
|
renderRepToasts(ImGui::GetIO().DeltaTime);
|
2026-03-12 04:53:03 -07:00
|
|
|
|
renderQuestCompleteToasts(ImGui::GetIO().DeltaTime);
|
2026-03-12 05:44:25 -07:00
|
|
|
|
renderZoneToasts(ImGui::GetIO().DeltaTime);
|
2026-03-12 11:06:40 -07:00
|
|
|
|
renderAreaTriggerToasts(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);
|
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
|
|
|
|
renderGroupInvitePopup(gameHandler);
|
2026-03-09 13:58:02 -07:00
|
|
|
|
renderDuelRequestPopup(gameHandler);
|
2026-03-12 05:06:14 -07:00
|
|
|
|
renderDuelCountdown(gameHandler);
|
2026-03-09 14:01:27 -07:00
|
|
|
|
renderLootRollPopup(gameHandler);
|
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
|
|
|
|
renderTradeRequestPopup(gameHandler);
|
2026-03-11 00:44:07 -07:00
|
|
|
|
renderTradeWindow(gameHandler);
|
2026-03-09 14:07:50 -07:00
|
|
|
|
renderSummonRequestPopup(gameHandler);
|
2026-03-09 14:14:15 -07:00
|
|
|
|
renderSharedQuestPopup(gameHandler);
|
2026-03-09 14:15:59 -07:00
|
|
|
|
renderItemTextWindow(gameHandler);
|
2026-02-13 21:39:48 -08:00
|
|
|
|
renderGuildInvitePopup(gameHandler);
|
2026-03-09 14:48:30 -07:00
|
|
|
|
renderReadyCheckPopup(gameHandler);
|
2026-03-10 21:12:28 -07:00
|
|
|
|
renderBgInvitePopup(gameHandler);
|
2026-03-12 22:25:46 -07:00
|
|
|
|
renderBfMgrInvitePopup(gameHandler);
|
2026-03-10 21:40:21 -07:00
|
|
|
|
renderLfgProposalPopup(gameHandler);
|
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);
|
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-02-09 23:41:38 -08:00
|
|
|
|
// renderQuestMarkers(gameHandler); // Disabled - using 3D billboard markers now
|
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-02-07 23:12:24 -08:00
|
|
|
|
renderResurrectDialog(gameHandler);
|
2026-03-10 12:53:05 -07:00
|
|
|
|
renderTalentWipeConfirmDialog(gameHandler);
|
2026-03-17 21:13:27 -07:00
|
|
|
|
renderPetUnlearnConfirmDialog(gameHandler);
|
2026-02-14 14:30:09 -08:00
|
|
|
|
renderChatBubbles(gameHandler);
|
2026-02-05 16:01:38 -08:00
|
|
|
|
renderEscapeMenu();
|
2026-02-05 16:11:00 -08:00
|
|
|
|
renderSettingsWindow();
|
2026-02-17 17:23:42 -08:00
|
|
|
|
renderDingEffect();
|
2026-03-09 13:53:42 -07:00
|
|
|
|
renderAchievementToast();
|
2026-03-12 15:42:55 -07:00
|
|
|
|
renderDiscoveryToast();
|
2026-03-12 15:53:45 -07:00
|
|
|
|
renderWhisperToasts();
|
2026-03-12 15:57:09 -07:00
|
|
|
|
renderQuestProgressToasts();
|
2026-03-12 16:12:21 -07:00
|
|
|
|
renderPlayerLevelUpToasts(gameHandler);
|
2026-03-12 16:19:25 -07:00
|
|
|
|
renderPvpHonorToasts();
|
2026-03-12 16:24:11 -07:00
|
|
|
|
renderItemLootToasts();
|
2026-03-12 16:33:08 -07:00
|
|
|
|
renderResurrectFlash();
|
2026-03-17 19:14:17 -07:00
|
|
|
|
renderZoneText(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()) {
|
|
|
|
|
|
size_t curLen = strlen(chatInputBuffer);
|
|
|
|
|
|
if (curLen + pendingSpellLink.size() + 1 < sizeof(chatInputBuffer)) {
|
|
|
|
|
|
strncat(chatInputBuffer, pendingSpellLink.c_str(), sizeof(chatInputBuffer) - curLen - 1);
|
|
|
|
|
|
chatInputMoveCursorToEnd = true;
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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()) {
|
|
|
|
|
|
size_t curLen = strlen(chatInputBuffer);
|
|
|
|
|
|
if (curLen + pendingLink.size() + 1 < sizeof(chatInputBuffer)) {
|
|
|
|
|
|
strncat(chatInputBuffer, pendingLink.c_str(), sizeof(chatInputBuffer) - curLen - 1);
|
|
|
|
|
|
chatInputMoveCursorToEnd = true;
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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-12 03:21:49 -07:00
|
|
|
|
if (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-12 07:15:08 -07:00
|
|
|
|
if (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
|
|
|
|
|
|
if (levelUpFlashAlpha_ > 0.0f) {
|
|
|
|
|
|
levelUpFlashAlpha_ -= ImGui::GetIO().DeltaTime * 1.0f; // fade over ~1 second
|
|
|
|
|
|
if (levelUpFlashAlpha_ < 0.0f) levelUpFlashAlpha_ = 0.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImDrawList* fg = ImGui::GetForegroundDrawList();
|
|
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
|
|
const float W = io.DisplaySize.x;
|
|
|
|
|
|
const float H = io.DisplaySize.y;
|
|
|
|
|
|
const int alpha = static_cast<int>(levelUpFlashAlpha_ * 160.0f);
|
|
|
|
|
|
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
|
|
|
|
|
|
if (levelUpFlashAlpha_ > 0.5f && levelUpDisplayLevel_ > 0) {
|
|
|
|
|
|
char lvlText[32];
|
|
|
|
|
|
snprintf(lvlText, sizeof(lvlText), "Level %u!", levelUpDisplayLevel_);
|
|
|
|
|
|
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:
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "In World");
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::WorldState::AUTHENTICATED:
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Authenticated");
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::WorldState::ENTERING_WORLD:
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Entering World...");
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "State: %d", static_cast<int>(state));
|
|
|
|
|
|
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:
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "Player");
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::ObjectType::UNIT:
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Unit");
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
|
2026-02-04 11:31:08 -08:00
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
2026-03-12 06:30:30 -07:00
|
|
|
|
auto* assetMgr = core::Application::getInstance().getAssetManager();
|
2026-02-04 11:31:08 -08:00
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
|
|
|
|
|
float chatW = std::min(500.0f, screenW * 0.4f);
|
|
|
|
|
|
float chatH = 220.0f;
|
|
|
|
|
|
float chatX = 8.0f;
|
|
|
|
|
|
float chatY = screenH - chatH - 80.0f; // Above action bar
|
2026-02-06 18:34:45 -08:00
|
|
|
|
if (chatWindowLocked) {
|
2026-02-17 03:50:36 -08:00
|
|
|
|
// Always recompute position from current window size when locked
|
|
|
|
|
|
chatWindowPos_ = ImVec2(chatX, chatY);
|
2026-02-06 18:34:45 -08:00
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(chatW, chatH), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowPos(chatWindowPos_, ImGuiCond_Always);
|
|
|
|
|
|
} else {
|
2026-02-17 03:50:36 -08:00
|
|
|
|
if (!chatWindowPosInit_) {
|
|
|
|
|
|
chatWindowPos_ = ImVec2(chatX, chatY);
|
|
|
|
|
|
chatWindowPosInit_ = true;
|
|
|
|
|
|
}
|
2026-02-06 18:34:45 -08:00
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(chatW, chatH), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
ImGui::SetNextWindowPos(chatWindowPos_, ImGuiCond_FirstUseEver);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize;
|
2026-02-07 21:12:54 -08:00
|
|
|
|
if (chatWindowLocked) {
|
|
|
|
|
|
flags |= ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar;
|
|
|
|
|
|
}
|
2026-02-06 18:34:45 -08:00
|
|
|
|
ImGui::Begin("Chat", nullptr, flags);
|
|
|
|
|
|
|
|
|
|
|
|
if (!chatWindowLocked) {
|
|
|
|
|
|
chatWindowPos_ = ImGui::GetWindowPos();
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-03-12 11:23:01 -07:00
|
|
|
|
// Update unread counts: scan any new messages since last frame
|
|
|
|
|
|
{
|
|
|
|
|
|
const auto& history = gameHandler.getChatHistory();
|
|
|
|
|
|
// Ensure unread array is sized correctly (guards against late init)
|
|
|
|
|
|
if (chatTabUnread_.size() != chatTabs_.size())
|
|
|
|
|
|
chatTabUnread_.assign(chatTabs_.size(), 0);
|
|
|
|
|
|
// If history shrank (e.g. cleared), reset
|
|
|
|
|
|
if (chatTabSeenCount_ > history.size()) chatTabSeenCount_ = 0;
|
|
|
|
|
|
for (size_t mi = chatTabSeenCount_; mi < history.size(); ++mi) {
|
|
|
|
|
|
const auto& msg = history[mi];
|
|
|
|
|
|
// For each non-General (non-0) tab that isn't currently active, check visibility
|
|
|
|
|
|
for (int ti = 1; ti < static_cast<int>(chatTabs_.size()); ++ti) {
|
|
|
|
|
|
if (ti == activeChatTab_) continue;
|
|
|
|
|
|
if (shouldShowMessage(msg, ti)) {
|
|
|
|
|
|
chatTabUnread_[ti]++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
chatTabSeenCount_ = history.size();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 14:30:09 -08:00
|
|
|
|
// Chat tabs
|
|
|
|
|
|
if (ImGui::BeginTabBar("ChatTabs")) {
|
|
|
|
|
|
for (int i = 0; i < static_cast<int>(chatTabs_.size()); ++i) {
|
2026-03-12 11:23:01 -07:00
|
|
|
|
// Build label with unread count suffix for non-General tabs
|
|
|
|
|
|
std::string tabLabel = chatTabs_[i].name;
|
|
|
|
|
|
if (i > 0 && i < static_cast<int>(chatTabUnread_.size()) && chatTabUnread_[i] > 0) {
|
|
|
|
|
|
tabLabel += " (" + std::to_string(chatTabUnread_[i]) + ")";
|
|
|
|
|
|
}
|
|
|
|
|
|
// Use ImGuiTabItemFlags_NoPushId so label changes don't break tab identity
|
|
|
|
|
|
if (ImGui::BeginTabItem(tabLabel.c_str())) {
|
|
|
|
|
|
if (activeChatTab_ != i) {
|
|
|
|
|
|
activeChatTab_ = i;
|
|
|
|
|
|
// Clear unread count when tab becomes active
|
|
|
|
|
|
if (i < static_cast<int>(chatTabUnread_.size()))
|
|
|
|
|
|
chatTabUnread_[i] = 0;
|
|
|
|
|
|
}
|
2026-02-14 14:30:09 -08:00
|
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTabBar();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
// Chat history
|
|
|
|
|
|
const auto& chatHistory = gameHandler.getChatHistory();
|
|
|
|
|
|
|
2026-02-14 18:27:59 -08:00
|
|
|
|
// Apply chat font size scaling
|
|
|
|
|
|
float chatScale = chatFontSize_ == 0 ? 0.85f : (chatFontSize_ == 2 ? 1.2f : 1.0f);
|
|
|
|
|
|
ImGui::SetWindowFontScale(chatScale);
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
ImGui::BeginChild("ChatHistory", ImVec2(0, -70), true, ImGuiWindowFlags_HorizontalScrollbar);
|
2026-02-07 21:12:54 -08:00
|
|
|
|
bool chatHistoryHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-14 15:58:54 -08:00
|
|
|
|
// Helper: parse WoW color code |cAARRGGBB → ImVec4
|
|
|
|
|
|
auto parseWowColor = [](const std::string& text, size_t pos) -> ImVec4 {
|
|
|
|
|
|
// |cAARRGGBB (10 chars total: |c + 8 hex)
|
|
|
|
|
|
if (pos + 10 > text.size()) return ImVec4(1, 1, 1, 1);
|
|
|
|
|
|
auto hexByte = [&](size_t offset) -> float {
|
|
|
|
|
|
const char* s = text.c_str() + pos + offset;
|
|
|
|
|
|
char buf[3] = {s[0], s[1], '\0'};
|
|
|
|
|
|
return static_cast<float>(strtol(buf, nullptr, 16)) / 255.0f;
|
|
|
|
|
|
};
|
|
|
|
|
|
float a = hexByte(2);
|
|
|
|
|
|
float r = hexByte(4);
|
|
|
|
|
|
float g = hexByte(6);
|
|
|
|
|
|
float b = hexByte(8);
|
|
|
|
|
|
return ImVec4(r, g, b, a);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Helper: render an item tooltip from ItemQueryResponseData
|
|
|
|
|
|
auto renderItemLinkTooltip = [&](uint32_t itemEntry) {
|
|
|
|
|
|
const auto* info = gameHandler.getItemInfo(itemEntry);
|
|
|
|
|
|
if (!info || !info->valid) return;
|
2026-02-19 01:50:50 -08:00
|
|
|
|
auto findComparableEquipped = [&](uint8_t inventoryType) -> const game::ItemSlot* {
|
|
|
|
|
|
using ES = game::EquipSlot;
|
|
|
|
|
|
const auto& inv = gameHandler.getInventory();
|
|
|
|
|
|
auto slotPtr = [&](ES slot) -> const game::ItemSlot* {
|
|
|
|
|
|
const auto& s = inv.getEquipSlot(slot);
|
|
|
|
|
|
return s.empty() ? nullptr : &s;
|
|
|
|
|
|
};
|
|
|
|
|
|
switch (inventoryType) {
|
|
|
|
|
|
case 1: return slotPtr(ES::HEAD);
|
|
|
|
|
|
case 2: return slotPtr(ES::NECK);
|
|
|
|
|
|
case 3: return slotPtr(ES::SHOULDERS);
|
|
|
|
|
|
case 4: return slotPtr(ES::SHIRT);
|
|
|
|
|
|
case 5:
|
|
|
|
|
|
case 20: return slotPtr(ES::CHEST);
|
|
|
|
|
|
case 6: return slotPtr(ES::WAIST);
|
|
|
|
|
|
case 7: return slotPtr(ES::LEGS);
|
|
|
|
|
|
case 8: return slotPtr(ES::FEET);
|
|
|
|
|
|
case 9: return slotPtr(ES::WRISTS);
|
|
|
|
|
|
case 10: return slotPtr(ES::HANDS);
|
|
|
|
|
|
case 11: {
|
|
|
|
|
|
if (auto* s = slotPtr(ES::RING1)) return s;
|
|
|
|
|
|
return slotPtr(ES::RING2);
|
|
|
|
|
|
}
|
|
|
|
|
|
case 12: {
|
|
|
|
|
|
if (auto* s = slotPtr(ES::TRINKET1)) return s;
|
|
|
|
|
|
return slotPtr(ES::TRINKET2);
|
|
|
|
|
|
}
|
|
|
|
|
|
case 13:
|
|
|
|
|
|
if (auto* s = slotPtr(ES::MAIN_HAND)) return s;
|
|
|
|
|
|
return slotPtr(ES::OFF_HAND);
|
|
|
|
|
|
case 14:
|
|
|
|
|
|
case 22:
|
|
|
|
|
|
case 23: return slotPtr(ES::OFF_HAND);
|
|
|
|
|
|
case 15:
|
|
|
|
|
|
case 25:
|
|
|
|
|
|
case 26: return slotPtr(ES::RANGED);
|
|
|
|
|
|
case 16: return slotPtr(ES::BACK);
|
|
|
|
|
|
case 17:
|
|
|
|
|
|
case 21: return slotPtr(ES::MAIN_HAND);
|
|
|
|
|
|
case 18:
|
|
|
|
|
|
for (int i = 0; i < game::Inventory::NUM_BAG_SLOTS; ++i) {
|
|
|
|
|
|
auto slot = static_cast<ES>(static_cast<int>(ES::BAG1) + i);
|
|
|
|
|
|
if (auto* s = slotPtr(slot)) return s;
|
|
|
|
|
|
}
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
case 19: return slotPtr(ES::TABARD);
|
|
|
|
|
|
default: return nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-02-14 15:58:54 -08:00
|
|
|
|
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
// Quality color for name
|
|
|
|
|
|
ImVec4 qColor(1, 1, 1, 1);
|
|
|
|
|
|
switch (info->quality) {
|
|
|
|
|
|
case 0: qColor = ImVec4(0.62f, 0.62f, 0.62f, 1.0f); break; // Poor
|
|
|
|
|
|
case 1: qColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); break; // Common
|
|
|
|
|
|
case 2: qColor = ImVec4(0.12f, 1.0f, 0.0f, 1.0f); break; // Uncommon
|
|
|
|
|
|
case 3: qColor = ImVec4(0.0f, 0.44f, 0.87f, 1.0f); break; // Rare
|
|
|
|
|
|
case 4: qColor = ImVec4(0.64f, 0.21f, 0.93f, 1.0f); break; // Epic
|
|
|
|
|
|
case 5: qColor = ImVec4(1.0f, 0.50f, 0.0f, 1.0f); break; // Legendary
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::TextColored(qColor, "%s", info->name.c_str());
|
|
|
|
|
|
|
2026-03-17 16:56:37 -07:00
|
|
|
|
// Heroic indicator (green, matches WoW tooltip style)
|
|
|
|
|
|
constexpr uint32_t kFlagHeroic = 0x8;
|
|
|
|
|
|
constexpr uint32_t kFlagUniqueEquipped = 0x1000000;
|
|
|
|
|
|
if (info->itemFlags & kFlagHeroic)
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.0f, 0.8f, 0.0f, 1.0f), "Heroic");
|
|
|
|
|
|
|
2026-03-17 14:41:00 -07:00
|
|
|
|
// Bind type (appears right under name in WoW)
|
|
|
|
|
|
switch (info->bindType) {
|
|
|
|
|
|
case 1: ImGui::TextDisabled("Binds when picked up"); break;
|
|
|
|
|
|
case 2: ImGui::TextDisabled("Binds when equipped"); break;
|
|
|
|
|
|
case 3: ImGui::TextDisabled("Binds when used"); break;
|
|
|
|
|
|
case 4: ImGui::TextDisabled("Quest Item"); break;
|
|
|
|
|
|
}
|
2026-03-17 16:56:37 -07:00
|
|
|
|
// Unique / Unique-Equipped
|
2026-03-17 14:41:00 -07:00
|
|
|
|
if (info->maxCount == 1)
|
2026-03-17 16:56:37 -07:00
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Unique");
|
|
|
|
|
|
else if (info->itemFlags & kFlagUniqueEquipped)
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Unique-Equipped");
|
2026-03-17 14:41:00 -07:00
|
|
|
|
|
2026-02-14 15:58:54 -08:00
|
|
|
|
// Slot type
|
|
|
|
|
|
if (info->inventoryType > 0) {
|
|
|
|
|
|
const char* slotName = "";
|
|
|
|
|
|
switch (info->inventoryType) {
|
|
|
|
|
|
case 1: slotName = "Head"; break;
|
|
|
|
|
|
case 2: slotName = "Neck"; break;
|
|
|
|
|
|
case 3: slotName = "Shoulder"; break;
|
|
|
|
|
|
case 4: slotName = "Shirt"; break;
|
|
|
|
|
|
case 5: slotName = "Chest"; break;
|
|
|
|
|
|
case 6: slotName = "Waist"; break;
|
|
|
|
|
|
case 7: slotName = "Legs"; break;
|
|
|
|
|
|
case 8: slotName = "Feet"; break;
|
|
|
|
|
|
case 9: slotName = "Wrist"; break;
|
|
|
|
|
|
case 10: slotName = "Hands"; break;
|
|
|
|
|
|
case 11: slotName = "Finger"; break;
|
|
|
|
|
|
case 12: slotName = "Trinket"; break;
|
|
|
|
|
|
case 13: slotName = "One-Hand"; break;
|
|
|
|
|
|
case 14: slotName = "Shield"; break;
|
|
|
|
|
|
case 15: slotName = "Ranged"; break;
|
|
|
|
|
|
case 16: slotName = "Back"; break;
|
|
|
|
|
|
case 17: slotName = "Two-Hand"; break;
|
|
|
|
|
|
case 18: slotName = "Bag"; break;
|
|
|
|
|
|
case 19: slotName = "Tabard"; break;
|
|
|
|
|
|
case 20: slotName = "Robe"; break;
|
|
|
|
|
|
case 21: slotName = "Main Hand"; break;
|
|
|
|
|
|
case 22: slotName = "Off Hand"; break;
|
|
|
|
|
|
case 23: slotName = "Held In Off-hand"; break;
|
|
|
|
|
|
case 25: slotName = "Thrown"; break;
|
|
|
|
|
|
case 26: slotName = "Ranged"; break;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (slotName[0]) {
|
|
|
|
|
|
if (!info->subclassName.empty())
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s %s", slotName, info->subclassName.c_str());
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", slotName);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-19 02:27:01 -08:00
|
|
|
|
auto isWeaponInventoryType = [](uint32_t invType) {
|
|
|
|
|
|
switch (invType) {
|
|
|
|
|
|
case 13: // One-Hand
|
|
|
|
|
|
case 15: // Ranged
|
|
|
|
|
|
case 17: // Two-Hand
|
|
|
|
|
|
case 21: // Main Hand
|
|
|
|
|
|
case 25: // Thrown
|
|
|
|
|
|
case 26: // Ranged Right
|
|
|
|
|
|
return true;
|
|
|
|
|
|
default:
|
|
|
|
|
|
return false;
|
2026-02-18 03:46:03 -08:00
|
|
|
|
}
|
2026-02-19 02:27:01 -08:00
|
|
|
|
};
|
|
|
|
|
|
const bool isWeapon = isWeaponInventoryType(info->inventoryType);
|
|
|
|
|
|
|
2026-03-17 14:41:00 -07:00
|
|
|
|
// Item level (after slot/subclass)
|
|
|
|
|
|
if (info->itemLevel > 0)
|
|
|
|
|
|
ImGui::TextDisabled("Item Level %u", info->itemLevel);
|
|
|
|
|
|
|
2026-02-19 02:27:01 -08:00
|
|
|
|
if (isWeapon && info->damageMax > 0.0f && info->delayMs > 0) {
|
|
|
|
|
|
float speed = static_cast<float>(info->delayMs) / 1000.0f;
|
|
|
|
|
|
float dps = ((info->damageMin + info->damageMax) * 0.5f) / speed;
|
2026-03-17 14:41:00 -07:00
|
|
|
|
// WoW-style: "22 - 41 Damage" with speed right-aligned on same row
|
|
|
|
|
|
char dmgBuf[64], spdBuf[32];
|
|
|
|
|
|
std::snprintf(dmgBuf, sizeof(dmgBuf), "%d - %d Damage",
|
|
|
|
|
|
static_cast<int>(info->damageMin), static_cast<int>(info->damageMax));
|
|
|
|
|
|
std::snprintf(spdBuf, sizeof(spdBuf), "Speed %.2f", speed);
|
|
|
|
|
|
float spdW = ImGui::CalcTextSize(spdBuf).x;
|
|
|
|
|
|
ImGui::Text("%s", dmgBuf);
|
|
|
|
|
|
ImGui::SameLine(ImGui::GetWindowWidth() - spdW - 16.0f);
|
|
|
|
|
|
ImGui::Text("%s", spdBuf);
|
|
|
|
|
|
ImGui::TextDisabled("(%.1f damage per second)", dps);
|
2026-02-18 03:46:03 -08:00
|
|
|
|
}
|
2026-02-14 15:58:54 -08:00
|
|
|
|
ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f);
|
2026-02-19 02:27:01 -08:00
|
|
|
|
auto appendBonus = [](std::string& out, int32_t val, const char* shortName) {
|
|
|
|
|
|
if (val <= 0) return;
|
|
|
|
|
|
if (!out.empty()) out += " ";
|
|
|
|
|
|
out += "+" + std::to_string(val) + " ";
|
|
|
|
|
|
out += shortName;
|
2026-02-14 15:58:54 -08:00
|
|
|
|
};
|
2026-02-19 02:27:01 -08:00
|
|
|
|
std::string bonusLine;
|
|
|
|
|
|
appendBonus(bonusLine, info->strength, "Str");
|
|
|
|
|
|
appendBonus(bonusLine, info->agility, "Agi");
|
|
|
|
|
|
appendBonus(bonusLine, info->stamina, "Sta");
|
|
|
|
|
|
appendBonus(bonusLine, info->intellect, "Int");
|
|
|
|
|
|
appendBonus(bonusLine, info->spirit, "Spi");
|
|
|
|
|
|
if (!bonusLine.empty()) {
|
|
|
|
|
|
ImGui::TextColored(green, "%s", bonusLine.c_str());
|
|
|
|
|
|
}
|
2026-02-19 05:48:40 -08:00
|
|
|
|
if (info->armor > 0) {
|
2026-02-19 02:27:01 -08:00
|
|
|
|
ImGui::Text("%d Armor", info->armor);
|
|
|
|
|
|
}
|
2026-03-17 16:54:40 -07:00
|
|
|
|
// Elemental resistances (fire resist gear, nature resist gear, etc.)
|
|
|
|
|
|
{
|
|
|
|
|
|
const int32_t resVals[6] = {
|
|
|
|
|
|
info->holyRes, info->fireRes, info->natureRes,
|
|
|
|
|
|
info->frostRes, info->shadowRes, info->arcaneRes
|
|
|
|
|
|
};
|
|
|
|
|
|
static const char* resLabels[6] = {
|
|
|
|
|
|
"Holy Resistance", "Fire Resistance", "Nature Resistance",
|
|
|
|
|
|
"Frost Resistance", "Shadow Resistance", "Arcane Resistance"
|
|
|
|
|
|
};
|
|
|
|
|
|
for (int ri = 0; ri < 6; ++ri)
|
|
|
|
|
|
if (resVals[ri] > 0) ImGui::Text("+%d %s", resVals[ri], resLabels[ri]);
|
|
|
|
|
|
}
|
2026-03-17 14:42:00 -07:00
|
|
|
|
// Extra stats (hit/crit/haste/sp/ap/expertise/resilience/etc.)
|
|
|
|
|
|
if (!info->extraStats.empty()) {
|
|
|
|
|
|
auto statName = [](uint32_t t) -> const char* {
|
|
|
|
|
|
switch (t) {
|
|
|
|
|
|
case 12: return "Defense Rating";
|
|
|
|
|
|
case 13: return "Dodge Rating";
|
|
|
|
|
|
case 14: return "Parry Rating";
|
|
|
|
|
|
case 15: return "Block Rating";
|
|
|
|
|
|
case 16: case 17: case 18: case 31: return "Hit Rating";
|
|
|
|
|
|
case 19: case 20: case 21: case 32: return "Critical Strike Rating";
|
|
|
|
|
|
case 28: case 29: case 30: case 35: return "Haste Rating";
|
|
|
|
|
|
case 34: return "Resilience Rating";
|
|
|
|
|
|
case 36: return "Expertise Rating";
|
|
|
|
|
|
case 37: return "Attack Power";
|
|
|
|
|
|
case 38: return "Ranged Attack Power";
|
|
|
|
|
|
case 45: return "Spell Power";
|
|
|
|
|
|
case 46: return "Healing Power";
|
|
|
|
|
|
case 47: return "Spell Damage";
|
|
|
|
|
|
case 49: return "Mana per 5 sec.";
|
|
|
|
|
|
case 43: return "Spell Penetration";
|
|
|
|
|
|
case 44: return "Block Value";
|
|
|
|
|
|
default: return nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
for (const auto& es : info->extraStats) {
|
|
|
|
|
|
const char* nm = statName(es.statType);
|
|
|
|
|
|
if (nm && es.statValue > 0)
|
|
|
|
|
|
ImGui::TextColored(green, "+%d %s", es.statValue, nm);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-17 16:42:19 -07:00
|
|
|
|
// Gem sockets (WotLK only — socketColor != 0 means socket present)
|
|
|
|
|
|
// socketColor bitmask: 1=Meta, 2=Red, 4=Yellow, 8=Blue
|
|
|
|
|
|
{
|
|
|
|
|
|
static const struct { uint32_t mask; const char* label; ImVec4 col; } kSocketTypes[] = {
|
|
|
|
|
|
{ 1, "Meta Socket", { 0.7f, 0.7f, 0.9f, 1.0f } },
|
|
|
|
|
|
{ 2, "Red Socket", { 1.0f, 0.3f, 0.3f, 1.0f } },
|
|
|
|
|
|
{ 4, "Yellow Socket", { 1.0f, 0.9f, 0.3f, 1.0f } },
|
|
|
|
|
|
{ 8, "Blue Socket", { 0.3f, 0.6f, 1.0f, 1.0f } },
|
|
|
|
|
|
};
|
|
|
|
|
|
bool hasSocket = false;
|
|
|
|
|
|
for (int s = 0; s < 3; ++s) {
|
|
|
|
|
|
if (info->socketColor[s] == 0) continue;
|
|
|
|
|
|
if (!hasSocket) { ImGui::Spacing(); hasSocket = true; }
|
|
|
|
|
|
for (const auto& st : kSocketTypes) {
|
|
|
|
|
|
if (info->socketColor[s] & st.mask) {
|
|
|
|
|
|
ImGui::TextColored(st.col, "%s", st.label);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (hasSocket && info->socketBonus != 0) {
|
|
|
|
|
|
// Socket bonus ID maps to SpellItemEnchantment.dbc — lazy-load names
|
|
|
|
|
|
static std::unordered_map<uint32_t, std::string> s_enchantNames;
|
|
|
|
|
|
static bool s_enchantNamesLoaded = false;
|
|
|
|
|
|
if (!s_enchantNamesLoaded && assetMgr) {
|
|
|
|
|
|
s_enchantNamesLoaded = true;
|
|
|
|
|
|
auto dbc = assetMgr->loadDBC("SpellItemEnchantment.dbc");
|
|
|
|
|
|
if (dbc && dbc->isLoaded()) {
|
|
|
|
|
|
const auto* lay = pipeline::getActiveDBCLayout()
|
|
|
|
|
|
? pipeline::getActiveDBCLayout()->getLayout("SpellItemEnchantment") : nullptr;
|
|
|
|
|
|
uint32_t nameField = lay ? lay->field("Name") : 8u;
|
|
|
|
|
|
if (nameField == 0xFFFFFFFF) nameField = 8;
|
|
|
|
|
|
uint32_t fc = dbc->getFieldCount();
|
|
|
|
|
|
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
|
|
|
|
|
uint32_t eid = dbc->getUInt32(r, 0);
|
|
|
|
|
|
if (eid == 0 || nameField >= fc) continue;
|
|
|
|
|
|
std::string ename = dbc->getString(r, nameField);
|
|
|
|
|
|
if (!ename.empty()) s_enchantNames[eid] = std::move(ename);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
auto enchIt = s_enchantNames.find(info->socketBonus);
|
|
|
|
|
|
if (enchIt != s_enchantNames.end())
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.5f, 0.8f, 0.5f, 1.0f), "Socket Bonus: %s", enchIt->second.c_str());
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.5f, 0.8f, 0.5f, 1.0f), "Socket Bonus: (id %u)", info->socketBonus);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-17 16:43:57 -07:00
|
|
|
|
// Item set membership
|
|
|
|
|
|
if (info->itemSetId != 0) {
|
|
|
|
|
|
struct SetEntry {
|
|
|
|
|
|
std::string name;
|
|
|
|
|
|
std::array<uint32_t, 10> itemIds{};
|
|
|
|
|
|
std::array<uint32_t, 10> spellIds{};
|
|
|
|
|
|
std::array<uint32_t, 10> thresholds{};
|
|
|
|
|
|
};
|
|
|
|
|
|
static std::unordered_map<uint32_t, SetEntry> s_setData;
|
|
|
|
|
|
static bool s_setDataLoaded = false;
|
|
|
|
|
|
if (!s_setDataLoaded && assetMgr) {
|
|
|
|
|
|
s_setDataLoaded = true;
|
|
|
|
|
|
auto dbc = assetMgr->loadDBC("ItemSet.dbc");
|
|
|
|
|
|
if (dbc && dbc->isLoaded()) {
|
|
|
|
|
|
const auto* layout = pipeline::getActiveDBCLayout()
|
|
|
|
|
|
? pipeline::getActiveDBCLayout()->getLayout("ItemSet") : nullptr;
|
|
|
|
|
|
auto lf = [&](const char* k, uint32_t def) -> uint32_t {
|
|
|
|
|
|
return layout ? (*layout)[k] : def;
|
|
|
|
|
|
};
|
|
|
|
|
|
uint32_t idF = lf("ID", 0), nameF = lf("Name", 1);
|
|
|
|
|
|
static const char* itemKeys[10] = {"Item0","Item1","Item2","Item3","Item4","Item5","Item6","Item7","Item8","Item9"};
|
|
|
|
|
|
static const char* spellKeys[10] = {"Spell0","Spell1","Spell2","Spell3","Spell4","Spell5","Spell6","Spell7","Spell8","Spell9"};
|
|
|
|
|
|
static const char* thrKeys[10] = {"Threshold0","Threshold1","Threshold2","Threshold3","Threshold4","Threshold5","Threshold6","Threshold7","Threshold8","Threshold9"};
|
|
|
|
|
|
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
|
|
|
|
|
uint32_t id = dbc->getUInt32(r, idF);
|
|
|
|
|
|
if (!id) continue;
|
|
|
|
|
|
SetEntry e;
|
|
|
|
|
|
e.name = dbc->getString(r, nameF);
|
|
|
|
|
|
for (int i = 0; i < 10; ++i) {
|
|
|
|
|
|
e.itemIds[i] = dbc->getUInt32(r, layout ? (*layout)[itemKeys[i]] : uint32_t(18 + i));
|
|
|
|
|
|
e.spellIds[i] = dbc->getUInt32(r, layout ? (*layout)[spellKeys[i]] : uint32_t(28 + i));
|
|
|
|
|
|
e.thresholds[i] = dbc->getUInt32(r, layout ? (*layout)[thrKeys[i]] : uint32_t(38 + i));
|
|
|
|
|
|
}
|
|
|
|
|
|
s_setData[id] = std::move(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
const auto& inv = gameHandler.getInventory();
|
|
|
|
|
|
auto setIt = s_setData.find(info->itemSetId);
|
|
|
|
|
|
if (setIt != s_setData.end()) {
|
|
|
|
|
|
const SetEntry& se = setIt->second;
|
|
|
|
|
|
int equipped = 0, total = 0;
|
|
|
|
|
|
for (int i = 0; i < 10; ++i) {
|
|
|
|
|
|
if (se.itemIds[i] == 0) continue;
|
|
|
|
|
|
++total;
|
|
|
|
|
|
for (int sl = 0; sl < game::Inventory::NUM_EQUIP_SLOTS; sl++) {
|
|
|
|
|
|
const auto& eq = inv.getEquipSlot(static_cast<game::EquipSlot>(sl));
|
|
|
|
|
|
if (!eq.empty() && eq.item.itemId == se.itemIds[i]) { ++equipped; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (total > 0)
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f),
|
|
|
|
|
|
"%s (%d/%d)", se.name.empty() ? "Set" : se.name.c_str(), equipped, total);
|
|
|
|
|
|
else if (!se.name.empty())
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "%s", se.name.c_str());
|
|
|
|
|
|
for (int i = 0; i < 10; ++i) {
|
|
|
|
|
|
if (se.spellIds[i] == 0 || se.thresholds[i] == 0) continue;
|
|
|
|
|
|
const std::string& bname = gameHandler.getSpellName(se.spellIds[i]);
|
|
|
|
|
|
bool active = (equipped >= static_cast<int>(se.thresholds[i]));
|
|
|
|
|
|
ImVec4 col = active ? ImVec4(0.5f, 1.0f, 0.5f, 1.0f) : ImVec4(0.55f, 0.55f, 0.55f, 1.0f);
|
|
|
|
|
|
if (!bname.empty())
|
|
|
|
|
|
ImGui::TextColored(col, "(%u) %s", se.thresholds[i], bname.c_str());
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::TextColored(col, "(%u) Set Bonus", se.thresholds[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Set (id %u)", info->itemSetId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-17 14:43:22 -07:00
|
|
|
|
// Item spell effects (Use / Equip / Chance on Hit / Teaches)
|
|
|
|
|
|
for (const auto& sp : info->spells) {
|
|
|
|
|
|
if (sp.spellId == 0) continue;
|
|
|
|
|
|
const char* triggerLabel = nullptr;
|
|
|
|
|
|
switch (sp.spellTrigger) {
|
|
|
|
|
|
case 0: triggerLabel = "Use"; break;
|
|
|
|
|
|
case 1: triggerLabel = "Equip"; break;
|
|
|
|
|
|
case 2: triggerLabel = "Chance on Hit"; break;
|
|
|
|
|
|
case 5: triggerLabel = "Teaches"; break;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!triggerLabel) continue;
|
2026-03-17 16:54:40 -07:00
|
|
|
|
// Use full spell description if available (matches inventory tooltip style)
|
|
|
|
|
|
const std::string& spDesc = gameHandler.getSpellDescription(sp.spellId);
|
|
|
|
|
|
const std::string& spText = !spDesc.empty() ? spDesc
|
|
|
|
|
|
: gameHandler.getSpellName(sp.spellId);
|
|
|
|
|
|
if (!spText.empty()) {
|
|
|
|
|
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 300.0f);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.0f, 0.8f, 1.0f, 1.0f),
|
|
|
|
|
|
"%s: %s", triggerLabel, spText.c_str());
|
|
|
|
|
|
ImGui::PopTextWrapPos();
|
|
|
|
|
|
}
|
2026-03-17 14:43:22 -07:00
|
|
|
|
}
|
2026-03-17 14:41:00 -07:00
|
|
|
|
// Required level
|
|
|
|
|
|
if (info->requiredLevel > 1)
|
|
|
|
|
|
ImGui::TextDisabled("Requires Level %u", info->requiredLevel);
|
2026-03-17 16:47:33 -07:00
|
|
|
|
// Required skill (e.g. "Requires Blacksmithing (300)")
|
|
|
|
|
|
if (info->requiredSkill != 0 && info->requiredSkillRank > 0) {
|
|
|
|
|
|
static std::unordered_map<uint32_t, std::string> s_skillNames;
|
|
|
|
|
|
static bool s_skillNamesLoaded = false;
|
|
|
|
|
|
if (!s_skillNamesLoaded && assetMgr) {
|
|
|
|
|
|
s_skillNamesLoaded = true;
|
|
|
|
|
|
auto dbc = assetMgr->loadDBC("SkillLine.dbc");
|
|
|
|
|
|
if (dbc && dbc->isLoaded()) {
|
|
|
|
|
|
const auto* layout = pipeline::getActiveDBCLayout()
|
|
|
|
|
|
? pipeline::getActiveDBCLayout()->getLayout("SkillLine") : nullptr;
|
|
|
|
|
|
uint32_t idF = layout ? (*layout)["ID"] : 0u;
|
|
|
|
|
|
uint32_t nameF = layout ? (*layout)["Name"] : 2u;
|
|
|
|
|
|
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
|
|
|
|
|
uint32_t sid = dbc->getUInt32(r, idF);
|
|
|
|
|
|
if (!sid) continue;
|
|
|
|
|
|
std::string sname = dbc->getString(r, nameF);
|
|
|
|
|
|
if (!sname.empty()) s_skillNames[sid] = std::move(sname);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
uint32_t playerSkillVal = 0;
|
|
|
|
|
|
const auto& skills = gameHandler.getPlayerSkills();
|
|
|
|
|
|
auto skPit = skills.find(info->requiredSkill);
|
|
|
|
|
|
if (skPit != skills.end()) playerSkillVal = skPit->second.effectiveValue();
|
|
|
|
|
|
bool meetsSkill = (playerSkillVal == 0 || playerSkillVal >= info->requiredSkillRank);
|
|
|
|
|
|
ImVec4 skColor = meetsSkill ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : ImVec4(1.0f, 0.5f, 0.5f, 1.0f);
|
|
|
|
|
|
auto skIt = s_skillNames.find(info->requiredSkill);
|
|
|
|
|
|
if (skIt != s_skillNames.end())
|
|
|
|
|
|
ImGui::TextColored(skColor, "Requires %s (%u)", skIt->second.c_str(), info->requiredSkillRank);
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::TextColored(skColor, "Requires Skill %u (%u)", info->requiredSkill, info->requiredSkillRank);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Required reputation (e.g. "Requires Exalted with Argent Dawn")
|
|
|
|
|
|
if (info->requiredReputationFaction != 0 && info->requiredReputationRank > 0) {
|
|
|
|
|
|
static std::unordered_map<uint32_t, std::string> s_factionNames;
|
|
|
|
|
|
static bool s_factionNamesLoaded = false;
|
|
|
|
|
|
if (!s_factionNamesLoaded && assetMgr) {
|
|
|
|
|
|
s_factionNamesLoaded = true;
|
|
|
|
|
|
auto dbc = assetMgr->loadDBC("Faction.dbc");
|
|
|
|
|
|
if (dbc && dbc->isLoaded()) {
|
|
|
|
|
|
const auto* layout = pipeline::getActiveDBCLayout()
|
|
|
|
|
|
? pipeline::getActiveDBCLayout()->getLayout("Faction") : nullptr;
|
|
|
|
|
|
uint32_t idF = layout ? (*layout)["ID"] : 0u;
|
|
|
|
|
|
uint32_t nameF = layout ? (*layout)["Name"] : 20u;
|
|
|
|
|
|
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
|
|
|
|
|
uint32_t fid = dbc->getUInt32(r, idF);
|
|
|
|
|
|
if (!fid) continue;
|
|
|
|
|
|
std::string fname = dbc->getString(r, nameF);
|
|
|
|
|
|
if (!fname.empty()) s_factionNames[fid] = std::move(fname);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
static const char* kRepRankNames[] = {
|
|
|
|
|
|
"Hated", "Hostile", "Unfriendly", "Neutral",
|
|
|
|
|
|
"Friendly", "Honored", "Revered", "Exalted"
|
|
|
|
|
|
};
|
|
|
|
|
|
const char* rankName = (info->requiredReputationRank < 8)
|
|
|
|
|
|
? kRepRankNames[info->requiredReputationRank] : "Unknown";
|
|
|
|
|
|
auto fIt = s_factionNames.find(info->requiredReputationFaction);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.75f), "Requires %s with %s",
|
|
|
|
|
|
rankName,
|
|
|
|
|
|
fIt != s_factionNames.end() ? fIt->second.c_str() : "Unknown Faction");
|
|
|
|
|
|
}
|
|
|
|
|
|
// Class restriction (e.g. "Classes: Paladin, Warrior")
|
|
|
|
|
|
if (info->allowableClass != 0) {
|
|
|
|
|
|
static const struct { uint32_t mask; const char* name; } kClasses[] = {
|
|
|
|
|
|
{ 1, "Warrior" },
|
|
|
|
|
|
{ 2, "Paladin" },
|
|
|
|
|
|
{ 4, "Hunter" },
|
|
|
|
|
|
{ 8, "Rogue" },
|
|
|
|
|
|
{ 16, "Priest" },
|
|
|
|
|
|
{ 32, "Death Knight" },
|
|
|
|
|
|
{ 64, "Shaman" },
|
|
|
|
|
|
{ 128, "Mage" },
|
|
|
|
|
|
{ 256, "Warlock" },
|
|
|
|
|
|
{ 1024, "Druid" },
|
|
|
|
|
|
};
|
|
|
|
|
|
int matchCount = 0;
|
|
|
|
|
|
for (const auto& kc : kClasses)
|
|
|
|
|
|
if (info->allowableClass & kc.mask) ++matchCount;
|
|
|
|
|
|
if (matchCount > 0 && matchCount < 10) {
|
|
|
|
|
|
char classBuf[128] = "Classes: ";
|
|
|
|
|
|
bool first = true;
|
|
|
|
|
|
for (const auto& kc : kClasses) {
|
|
|
|
|
|
if (!(info->allowableClass & kc.mask)) continue;
|
|
|
|
|
|
if (!first) strncat(classBuf, ", ", sizeof(classBuf) - strlen(classBuf) - 1);
|
|
|
|
|
|
strncat(classBuf, kc.name, sizeof(classBuf) - strlen(classBuf) - 1);
|
|
|
|
|
|
first = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
uint8_t pc = gameHandler.getPlayerClass();
|
|
|
|
|
|
uint32_t pmask = (pc > 0 && pc <= 10) ? (1u << (pc - 1)) : 0u;
|
|
|
|
|
|
bool playerAllowed = (pmask == 0 || (info->allowableClass & pmask));
|
|
|
|
|
|
ImVec4 clColor = playerAllowed ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : ImVec4(1.0f, 0.5f, 0.5f, 1.0f);
|
|
|
|
|
|
ImGui::TextColored(clColor, "%s", classBuf);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Race restriction (e.g. "Races: Night Elf, Human")
|
|
|
|
|
|
if (info->allowableRace != 0) {
|
|
|
|
|
|
static const struct { uint32_t mask; const char* name; } kRaces[] = {
|
|
|
|
|
|
{ 1, "Human" },
|
|
|
|
|
|
{ 2, "Orc" },
|
|
|
|
|
|
{ 4, "Dwarf" },
|
|
|
|
|
|
{ 8, "Night Elf" },
|
|
|
|
|
|
{ 16, "Undead" },
|
|
|
|
|
|
{ 32, "Tauren" },
|
|
|
|
|
|
{ 64, "Gnome" },
|
|
|
|
|
|
{ 128, "Troll" },
|
|
|
|
|
|
{ 512, "Blood Elf" },
|
|
|
|
|
|
{ 1024, "Draenei" },
|
|
|
|
|
|
};
|
|
|
|
|
|
constexpr uint32_t kAllPlayable = 1|2|4|8|16|32|64|128|512|1024;
|
|
|
|
|
|
if ((info->allowableRace & kAllPlayable) != kAllPlayable) {
|
|
|
|
|
|
int matchCount = 0;
|
|
|
|
|
|
for (const auto& kr : kRaces)
|
|
|
|
|
|
if (info->allowableRace & kr.mask) ++matchCount;
|
|
|
|
|
|
if (matchCount > 0) {
|
|
|
|
|
|
char raceBuf[160] = "Races: ";
|
|
|
|
|
|
bool first = true;
|
|
|
|
|
|
for (const auto& kr : kRaces) {
|
|
|
|
|
|
if (!(info->allowableRace & kr.mask)) continue;
|
|
|
|
|
|
if (!first) strncat(raceBuf, ", ", sizeof(raceBuf) - strlen(raceBuf) - 1);
|
|
|
|
|
|
strncat(raceBuf, kr.name, sizeof(raceBuf) - strlen(raceBuf) - 1);
|
|
|
|
|
|
first = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
uint8_t pr = gameHandler.getPlayerRace();
|
|
|
|
|
|
uint32_t pmask = (pr > 0 && pr <= 11) ? (1u << (pr - 1)) : 0u;
|
|
|
|
|
|
bool playerAllowed = (pmask == 0 || (info->allowableRace & pmask));
|
|
|
|
|
|
ImVec4 rColor = playerAllowed ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : ImVec4(1.0f, 0.5f, 0.5f, 1.0f);
|
|
|
|
|
|
ImGui::TextColored(rColor, "%s", raceBuf);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-17 14:44:15 -07:00
|
|
|
|
// Flavor / lore text (shown in gold italic in WoW, use a yellow-ish dim color here)
|
|
|
|
|
|
if (!info->description.empty()) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::PushTextWrapPos(300.0f);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 0.85f), "\"%s\"", info->description.c_str());
|
|
|
|
|
|
ImGui::PopTextWrapPos();
|
|
|
|
|
|
}
|
2026-02-14 15:58:54 -08:00
|
|
|
|
if (info->sellPrice > 0) {
|
|
|
|
|
|
uint32_t g = info->sellPrice / 10000;
|
|
|
|
|
|
uint32_t s = (info->sellPrice / 100) % 100;
|
|
|
|
|
|
uint32_t c = info->sellPrice % 100;
|
2026-03-12 08:15:46 -07:00
|
|
|
|
ImGui::TextDisabled("Sell:"); ImGui::SameLine(0, 4);
|
|
|
|
|
|
renderCoinsText(g, s, c);
|
2026-02-14 15:58:54 -08:00
|
|
|
|
}
|
2026-02-19 01:50:50 -08:00
|
|
|
|
|
|
|
|
|
|
if (ImGui::GetIO().KeyShift && info->inventoryType > 0) {
|
|
|
|
|
|
if (const auto* eq = findComparableEquipped(static_cast<uint8_t>(info->inventoryType))) {
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::TextDisabled("Equipped:");
|
2026-02-22 03:32:08 -08:00
|
|
|
|
VkDescriptorSet eqIcon = inventoryScreen.getItemIcon(eq->item.displayInfoId);
|
2026-02-19 01:50:50 -08:00
|
|
|
|
if (eqIcon) {
|
|
|
|
|
|
ImGui::Image((ImTextureID)(uintptr_t)eqIcon, ImVec2(18.0f, 18.0f));
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::TextColored(InventoryScreen::getQualityColor(eq->item.quality), "%s", eq->item.name.c_str());
|
2026-02-19 02:27:01 -08:00
|
|
|
|
if (isWeaponInventoryType(eq->item.inventoryType) &&
|
|
|
|
|
|
eq->item.damageMax > 0.0f && eq->item.delayMs > 0) {
|
|
|
|
|
|
float speed = static_cast<float>(eq->item.delayMs) / 1000.0f;
|
|
|
|
|
|
float dps = ((eq->item.damageMin + eq->item.damageMax) * 0.5f) / speed;
|
2026-03-17 14:41:00 -07:00
|
|
|
|
char eqDmg[64], eqSpd[32];
|
|
|
|
|
|
std::snprintf(eqDmg, sizeof(eqDmg), "%d - %d Damage",
|
|
|
|
|
|
static_cast<int>(eq->item.damageMin), static_cast<int>(eq->item.damageMax));
|
|
|
|
|
|
std::snprintf(eqSpd, sizeof(eqSpd), "Speed %.2f", speed);
|
|
|
|
|
|
float eqSpdW = ImGui::CalcTextSize(eqSpd).x;
|
|
|
|
|
|
ImGui::Text("%s", eqDmg);
|
|
|
|
|
|
ImGui::SameLine(ImGui::GetWindowWidth() - eqSpdW - 16.0f);
|
|
|
|
|
|
ImGui::Text("%s", eqSpd);
|
|
|
|
|
|
ImGui::TextDisabled("(%.1f damage per second)", dps);
|
2026-02-19 02:27:01 -08:00
|
|
|
|
}
|
2026-02-19 05:48:40 -08:00
|
|
|
|
if (eq->item.armor > 0) {
|
2026-02-19 02:27:01 -08:00
|
|
|
|
ImGui::Text("%d Armor", eq->item.armor);
|
|
|
|
|
|
}
|
|
|
|
|
|
std::string eqBonusLine;
|
|
|
|
|
|
appendBonus(eqBonusLine, eq->item.strength, "Str");
|
|
|
|
|
|
appendBonus(eqBonusLine, eq->item.agility, "Agi");
|
|
|
|
|
|
appendBonus(eqBonusLine, eq->item.stamina, "Sta");
|
|
|
|
|
|
appendBonus(eqBonusLine, eq->item.intellect, "Int");
|
|
|
|
|
|
appendBonus(eqBonusLine, eq->item.spirit, "Spi");
|
|
|
|
|
|
if (!eqBonusLine.empty()) {
|
|
|
|
|
|
ImGui::TextColored(green, "%s", eqBonusLine.c_str());
|
2026-02-19 01:50:50 -08:00
|
|
|
|
}
|
2026-03-17 14:50:28 -07:00
|
|
|
|
// Extra stats for the equipped item
|
|
|
|
|
|
for (const auto& es : eq->item.extraStats) {
|
|
|
|
|
|
const char* nm = nullptr;
|
|
|
|
|
|
switch (es.statType) {
|
|
|
|
|
|
case 12: nm = "Defense Rating"; break;
|
|
|
|
|
|
case 13: nm = "Dodge Rating"; break;
|
|
|
|
|
|
case 14: nm = "Parry Rating"; break;
|
|
|
|
|
|
case 16: case 17: case 18: case 31: nm = "Hit Rating"; break;
|
|
|
|
|
|
case 19: case 20: case 21: case 32: nm = "Critical Strike Rating"; break;
|
|
|
|
|
|
case 28: case 29: case 30: case 35: nm = "Haste Rating"; break;
|
|
|
|
|
|
case 34: nm = "Resilience Rating"; break;
|
|
|
|
|
|
case 36: nm = "Expertise Rating"; break;
|
|
|
|
|
|
case 37: nm = "Attack Power"; break;
|
|
|
|
|
|
case 38: nm = "Ranged Attack Power"; break;
|
|
|
|
|
|
case 45: nm = "Spell Power"; break;
|
|
|
|
|
|
case 46: nm = "Healing Power"; break;
|
|
|
|
|
|
case 49: nm = "Mana per 5 sec."; break;
|
|
|
|
|
|
default: break;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (nm && es.statValue > 0)
|
|
|
|
|
|
ImGui::TextColored(green, "+%d %s", es.statValue, nm);
|
|
|
|
|
|
}
|
2026-02-19 01:50:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-14 15:58:54 -08:00
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Helper: render text with clickable URLs and WoW item links
|
|
|
|
|
|
auto renderTextWithLinks = [&](const std::string& text, const ImVec4& color) {
|
2026-02-07 23:32:27 -08:00
|
|
|
|
size_t pos = 0;
|
|
|
|
|
|
while (pos < text.size()) {
|
2026-02-14 15:58:54 -08:00
|
|
|
|
// Find next special element: URL or WoW link
|
2026-02-19 16:17:06 -08:00
|
|
|
|
size_t urlStart = text.find("https://", pos);
|
2026-02-07 23:32:27 -08:00
|
|
|
|
|
2026-03-12 06:30:30 -07:00
|
|
|
|
// Find next WoW link (may be colored with |c prefix or bare |H)
|
2026-02-14 15:58:54 -08:00
|
|
|
|
size_t linkStart = text.find("|c", pos);
|
2026-03-12 06:30:30 -07:00
|
|
|
|
// Also handle bare |H links without color prefix
|
|
|
|
|
|
size_t bareItem = text.find("|Hitem:", pos);
|
|
|
|
|
|
size_t bareSpell = text.find("|Hspell:", pos);
|
|
|
|
|
|
size_t bareQuest = text.find("|Hquest:", pos);
|
|
|
|
|
|
size_t bareLinkStart = std::min({bareItem, bareSpell, bareQuest});
|
2026-02-14 15:58:54 -08:00
|
|
|
|
|
|
|
|
|
|
// Determine which comes first
|
|
|
|
|
|
size_t nextSpecial = std::min({urlStart, linkStart, bareLinkStart});
|
|
|
|
|
|
|
|
|
|
|
|
if (nextSpecial == std::string::npos) {
|
|
|
|
|
|
// No more special elements, render remaining text
|
2026-02-07 23:32:27 -08:00
|
|
|
|
std::string remaining = text.substr(pos);
|
|
|
|
|
|
if (!remaining.empty()) {
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, color);
|
|
|
|
|
|
ImGui::TextWrapped("%s", remaining.c_str());
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 15:58:54 -08:00
|
|
|
|
// Render plain text before special element
|
|
|
|
|
|
if (nextSpecial > pos) {
|
|
|
|
|
|
std::string before = text.substr(pos, nextSpecial - pos);
|
2026-02-07 23:32:27 -08:00
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, color);
|
|
|
|
|
|
ImGui::TextWrapped("%s", before.c_str());
|
|
|
|
|
|
ImGui::PopStyleColor();
|
2026-02-14 15:58:54 -08:00
|
|
|
|
ImGui::SameLine(0, 0);
|
2026-02-07 23:32:27 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 15:58:54 -08:00
|
|
|
|
// Handle WoW item link
|
|
|
|
|
|
if (nextSpecial == linkStart || nextSpecial == bareLinkStart) {
|
|
|
|
|
|
ImVec4 linkColor = color;
|
|
|
|
|
|
size_t hStart = std::string::npos;
|
|
|
|
|
|
|
|
|
|
|
|
if (nextSpecial == linkStart && text.size() > linkStart + 10) {
|
|
|
|
|
|
// Parse |cAARRGGBB color
|
|
|
|
|
|
linkColor = parseWowColor(text, linkStart);
|
2026-03-12 06:30:30 -07:00
|
|
|
|
// Find the nearest |H link of any supported type
|
|
|
|
|
|
size_t hItem = text.find("|Hitem:", linkStart + 10);
|
|
|
|
|
|
size_t hSpell = text.find("|Hspell:", linkStart + 10);
|
|
|
|
|
|
size_t hQuest = text.find("|Hquest:", linkStart + 10);
|
|
|
|
|
|
size_t hAch = text.find("|Hachievement:", linkStart + 10);
|
|
|
|
|
|
hStart = std::min({hItem, hSpell, hQuest, hAch});
|
2026-02-14 15:58:54 -08:00
|
|
|
|
} else if (nextSpecial == bareLinkStart) {
|
|
|
|
|
|
hStart = bareLinkStart;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (hStart != std::string::npos) {
|
2026-03-12 06:30:30 -07:00
|
|
|
|
// Determine link type
|
|
|
|
|
|
const bool isSpellLink = (text.compare(hStart, 8, "|Hspell:") == 0);
|
|
|
|
|
|
const bool isQuestLink = (text.compare(hStart, 8, "|Hquest:") == 0);
|
|
|
|
|
|
const bool isAchievLink = (text.compare(hStart, 14, "|Hachievement:") == 0);
|
|
|
|
|
|
// Default: item link
|
|
|
|
|
|
|
|
|
|
|
|
// Parse the first numeric ID after |Htype:
|
|
|
|
|
|
size_t idOffset = isSpellLink ? 8 : (isQuestLink ? 8 : (isAchievLink ? 14 : 7));
|
|
|
|
|
|
size_t entryStart = hStart + idOffset;
|
2026-02-14 15:58:54 -08:00
|
|
|
|
size_t entryEnd = text.find(':', entryStart);
|
2026-03-12 06:30:30 -07:00
|
|
|
|
uint32_t linkId = 0;
|
2026-02-14 15:58:54 -08:00
|
|
|
|
if (entryEnd != std::string::npos) {
|
2026-03-12 06:30:30 -07:00
|
|
|
|
linkId = static_cast<uint32_t>(strtoul(
|
2026-02-14 15:58:54 -08:00
|
|
|
|
text.substr(entryStart, entryEnd - entryStart).c_str(), nullptr, 10));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Find display name: |h[Name]|h
|
|
|
|
|
|
size_t nameTagStart = text.find("|h[", hStart);
|
|
|
|
|
|
size_t nameTagEnd = (nameTagStart != std::string::npos)
|
|
|
|
|
|
? text.find("]|h", nameTagStart + 3) : std::string::npos;
|
|
|
|
|
|
|
2026-03-12 06:30:30 -07:00
|
|
|
|
std::string linkName = isSpellLink ? "Unknown Spell"
|
|
|
|
|
|
: isQuestLink ? "Unknown Quest"
|
|
|
|
|
|
: isAchievLink ? "Unknown Achievement"
|
|
|
|
|
|
: "Unknown Item";
|
2026-02-14 15:58:54 -08:00
|
|
|
|
if (nameTagStart != std::string::npos && nameTagEnd != std::string::npos) {
|
2026-03-12 06:30:30 -07:00
|
|
|
|
linkName = text.substr(nameTagStart + 3, nameTagEnd - nameTagStart - 3);
|
2026-02-14 15:58:54 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Find end of entire link sequence (|r or after ]|h)
|
2026-03-12 06:30:30 -07:00
|
|
|
|
size_t linkEnd = (nameTagEnd != std::string::npos) ? nameTagEnd + 3 : hStart + idOffset;
|
2026-02-14 15:58:54 -08:00
|
|
|
|
size_t resetPos = text.find("|r", linkEnd);
|
|
|
|
|
|
if (resetPos != std::string::npos && resetPos <= linkEnd + 2) {
|
|
|
|
|
|
linkEnd = resetPos + 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 06:30:30 -07:00
|
|
|
|
if (!isSpellLink && !isQuestLink && !isAchievLink) {
|
|
|
|
|
|
// --- Item link ---
|
|
|
|
|
|
uint32_t itemEntry = linkId;
|
|
|
|
|
|
if (itemEntry > 0) {
|
|
|
|
|
|
gameHandler.ensureItemInfo(itemEntry);
|
|
|
|
|
|
}
|
2026-02-14 15:58:54 -08:00
|
|
|
|
|
2026-03-12 06:30:30 -07:00
|
|
|
|
// Show small icon before item link if available
|
|
|
|
|
|
if (itemEntry > 0) {
|
|
|
|
|
|
const auto* chatInfo = gameHandler.getItemInfo(itemEntry);
|
|
|
|
|
|
if (chatInfo && chatInfo->valid && chatInfo->displayInfoId != 0) {
|
|
|
|
|
|
VkDescriptorSet chatIcon = inventoryScreen.getItemIcon(chatInfo->displayInfoId);
|
|
|
|
|
|
if (chatIcon) {
|
|
|
|
|
|
ImGui::Image((ImTextureID)(uintptr_t)chatIcon, ImVec2(12, 12));
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
|
|
|
|
renderItemLinkTooltip(itemEntry);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine(0, 2);
|
2026-03-11 21:03:51 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 06:30:30 -07:00
|
|
|
|
// Render bracketed item name in quality color
|
|
|
|
|
|
std::string display = "[" + linkName + "]";
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, linkColor);
|
|
|
|
|
|
ImGui::TextWrapped("%s", display.c_str());
|
|
|
|
|
|
ImGui::PopStyleColor();
|
2026-02-14 15:58:54 -08:00
|
|
|
|
|
2026-03-12 06:30:30 -07:00
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
|
|
|
|
if (itemEntry > 0) {
|
|
|
|
|
|
renderItemLinkTooltip(itemEntry);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (isSpellLink) {
|
|
|
|
|
|
// --- Spell link: |Hspell:SPELLID:RANK|h[Name]|h ---
|
|
|
|
|
|
// Small icon (use spell icon cache if available)
|
|
|
|
|
|
VkDescriptorSet spellIcon = (linkId > 0) ? getSpellIcon(linkId, assetMgr) : VK_NULL_HANDLE;
|
|
|
|
|
|
if (spellIcon) {
|
|
|
|
|
|
ImGui::Image((ImTextureID)(uintptr_t)spellIcon, ImVec2(12, 12));
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
|
|
|
|
spellbookScreen.renderSpellInfoTooltip(linkId, gameHandler, assetMgr);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine(0, 2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string display = "[" + linkName + "]";
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, linkColor);
|
|
|
|
|
|
ImGui::TextWrapped("%s", display.c_str());
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
|
|
|
|
if (linkId > 0) {
|
|
|
|
|
|
spellbookScreen.renderSpellInfoTooltip(linkId, gameHandler, assetMgr);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (isQuestLink) {
|
|
|
|
|
|
// --- Quest link: |Hquest:QUESTID:QUESTLEVEL|h[Name]|h ---
|
|
|
|
|
|
std::string display = "[" + linkName + "]";
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.84f, 0.0f, 1.0f)); // gold
|
|
|
|
|
|
ImGui::TextWrapped("%s", display.c_str());
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "%s", linkName.c_str());
|
|
|
|
|
|
// Parse quest level (second field after questId)
|
|
|
|
|
|
if (entryEnd != std::string::npos) {
|
|
|
|
|
|
size_t lvlEnd = text.find(':', entryEnd + 1);
|
|
|
|
|
|
if (lvlEnd == std::string::npos) lvlEnd = text.find('|', entryEnd + 1);
|
|
|
|
|
|
if (lvlEnd != std::string::npos) {
|
|
|
|
|
|
uint32_t qLvl = static_cast<uint32_t>(strtoul(
|
|
|
|
|
|
text.substr(entryEnd + 1, lvlEnd - entryEnd - 1).c_str(), nullptr, 10));
|
|
|
|
|
|
if (qLvl > 0) ImGui::TextDisabled("Level %u Quest", qLvl);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::TextDisabled("Click quest log to view details");
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
|
|
|
|
|
// Click: open quest log and select this quest if we have it
|
|
|
|
|
|
if (ImGui::IsItemClicked() && linkId > 0) {
|
|
|
|
|
|
questLogScreen.openAndSelectQuest(linkId);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// --- Achievement link ---
|
|
|
|
|
|
std::string display = "[" + linkName + "]";
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f, 0.0f, 1.0f)); // gold
|
|
|
|
|
|
ImGui::TextWrapped("%s", display.c_str());
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
|
|
|
|
ImGui::SetTooltip("Achievement: %s", linkName.c_str());
|
2026-02-14 15:58:54 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 06:30:30 -07:00
|
|
|
|
// Shift-click: insert entire link back into chat input
|
2026-02-14 15:58:54 -08:00
|
|
|
|
if (ImGui::IsItemClicked() && ImGui::GetIO().KeyShift) {
|
|
|
|
|
|
std::string linkText = text.substr(nextSpecial, linkEnd - nextSpecial);
|
|
|
|
|
|
size_t curLen = strlen(chatInputBuffer);
|
|
|
|
|
|
if (curLen + linkText.size() + 1 < sizeof(chatInputBuffer)) {
|
|
|
|
|
|
strncat(chatInputBuffer, linkText.c_str(), sizeof(chatInputBuffer) - curLen - 1);
|
|
|
|
|
|
chatInputMoveCursorToEnd = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pos = linkEnd;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 16:42:47 -08:00
|
|
|
|
// Not an item link — treat as colored text: |cAARRGGBB...text...|r
|
|
|
|
|
|
if (nextSpecial == linkStart && text.size() > linkStart + 10) {
|
|
|
|
|
|
ImVec4 cColor = parseWowColor(text, linkStart);
|
|
|
|
|
|
size_t textStart = linkStart + 10; // after |cAARRGGBB
|
|
|
|
|
|
size_t resetPos2 = text.find("|r", textStart);
|
|
|
|
|
|
std::string coloredText;
|
|
|
|
|
|
if (resetPos2 != std::string::npos) {
|
|
|
|
|
|
coloredText = text.substr(textStart, resetPos2 - textStart);
|
|
|
|
|
|
pos = resetPos2 + 2; // skip |r
|
|
|
|
|
|
} else {
|
|
|
|
|
|
coloredText = text.substr(textStart);
|
|
|
|
|
|
pos = text.size();
|
|
|
|
|
|
}
|
|
|
|
|
|
// Strip any remaining WoW markup from the colored segment
|
|
|
|
|
|
// (e.g. |H...|h pairs that aren't item links)
|
|
|
|
|
|
std::string clean;
|
|
|
|
|
|
for (size_t i = 0; i < coloredText.size(); i++) {
|
|
|
|
|
|
if (coloredText[i] == '|' && i + 1 < coloredText.size()) {
|
|
|
|
|
|
char next = coloredText[i + 1];
|
|
|
|
|
|
if (next == 'H') {
|
|
|
|
|
|
// Skip |H...|h
|
|
|
|
|
|
size_t hEnd = coloredText.find("|h", i + 2);
|
|
|
|
|
|
if (hEnd != std::string::npos) { i = hEnd + 1; continue; }
|
|
|
|
|
|
} else if (next == 'h') {
|
|
|
|
|
|
i += 1; continue; // skip |h
|
|
|
|
|
|
} else if (next == 'r') {
|
|
|
|
|
|
i += 1; continue; // skip |r
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
clean += coloredText[i];
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!clean.empty()) {
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, cColor);
|
|
|
|
|
|
ImGui::TextWrapped("%s", clean.c_str());
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
ImGui::SameLine(0, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Bare |c without enough chars for color — render literally
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, color);
|
|
|
|
|
|
ImGui::TextWrapped("|c");
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
ImGui::SameLine(0, 0);
|
|
|
|
|
|
pos = nextSpecial + 2;
|
|
|
|
|
|
}
|
2026-02-14 15:58:54 -08:00
|
|
|
|
continue;
|
2026-02-07 23:32:27 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 15:58:54 -08:00
|
|
|
|
// Handle URL
|
|
|
|
|
|
if (nextSpecial == urlStart) {
|
|
|
|
|
|
size_t urlEnd = text.find_first_of(" \t\n\r", urlStart);
|
|
|
|
|
|
if (urlEnd == std::string::npos) urlEnd = text.size();
|
|
|
|
|
|
std::string url = text.substr(urlStart, urlEnd - urlStart);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.7f, 1.0f, 1.0f));
|
|
|
|
|
|
ImGui::TextWrapped("%s", url.c_str());
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
|
|
|
|
ImGui::SetTooltip("Open: %s", url.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::IsItemClicked()) {
|
|
|
|
|
|
std::string cmd = "xdg-open '" + url + "' &";
|
|
|
|
|
|
[[maybe_unused]] int result = system(cmd.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
|
|
|
|
|
|
pos = urlEnd;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-02-07 23:32:27 -08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-12 06:45:27 -07:00
|
|
|
|
// Determine local player name for mention detection (case-insensitive)
|
|
|
|
|
|
std::string selfNameLower;
|
|
|
|
|
|
{
|
|
|
|
|
|
const auto* ch = gameHandler.getActiveCharacter();
|
|
|
|
|
|
if (ch && !ch->name.empty()) {
|
|
|
|
|
|
selfNameLower = ch->name;
|
|
|
|
|
|
for (auto& c : selfNameLower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Scan NEW messages (beyond chatMentionSeenCount_) for mentions and play notification sound
|
|
|
|
|
|
if (!selfNameLower.empty() && chatHistory.size() > chatMentionSeenCount_) {
|
|
|
|
|
|
for (size_t mi = chatMentionSeenCount_; mi < chatHistory.size(); ++mi) {
|
|
|
|
|
|
const auto& mMsg = chatHistory[mi];
|
|
|
|
|
|
// Skip outgoing whispers, system, and monster messages
|
|
|
|
|
|
if (mMsg.type == game::ChatType::WHISPER_INFORM ||
|
|
|
|
|
|
mMsg.type == game::ChatType::SYSTEM) continue;
|
|
|
|
|
|
// Case-insensitive search in message body
|
|
|
|
|
|
std::string bodyLower = mMsg.message;
|
|
|
|
|
|
for (auto& c : bodyLower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
if (bodyLower.find(selfNameLower) != std::string::npos) {
|
|
|
|
|
|
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
|
|
|
|
|
if (auto* ui = renderer->getUiSoundManager())
|
|
|
|
|
|
ui->playWhisperReceived();
|
|
|
|
|
|
}
|
|
|
|
|
|
break; // play at most once per scan pass
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
chatMentionSeenCount_ = chatHistory.size();
|
|
|
|
|
|
} else if (chatHistory.size() <= chatMentionSeenCount_) {
|
|
|
|
|
|
chatMentionSeenCount_ = chatHistory.size(); // reset if history was cleared
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 15:53:45 -07:00
|
|
|
|
// Scan NEW messages for incoming whispers and push a toast notification
|
|
|
|
|
|
{
|
|
|
|
|
|
size_t histSize = chatHistory.size();
|
|
|
|
|
|
if (histSize < whisperSeenCount_) whisperSeenCount_ = histSize; // cleared
|
|
|
|
|
|
for (size_t wi = whisperSeenCount_; wi < histSize; ++wi) {
|
|
|
|
|
|
const auto& wMsg = chatHistory[wi];
|
|
|
|
|
|
if (wMsg.type == game::ChatType::WHISPER ||
|
|
|
|
|
|
wMsg.type == game::ChatType::RAID_BOSS_WHISPER) {
|
|
|
|
|
|
WhisperToastEntry toast;
|
|
|
|
|
|
toast.sender = wMsg.senderName;
|
|
|
|
|
|
if (toast.sender.empty() && wMsg.senderGuid != 0)
|
|
|
|
|
|
toast.sender = gameHandler.lookupName(wMsg.senderGuid);
|
|
|
|
|
|
if (toast.sender.empty()) toast.sender = "Unknown";
|
|
|
|
|
|
// Truncate preview to 60 chars
|
|
|
|
|
|
toast.preview = wMsg.message.size() > 60
|
|
|
|
|
|
? wMsg.message.substr(0, 57) + "..."
|
|
|
|
|
|
: wMsg.message;
|
|
|
|
|
|
toast.age = 0.0f;
|
|
|
|
|
|
// Keep at most 3 stacked toasts
|
|
|
|
|
|
if (whisperToasts_.size() >= 3) whisperToasts_.erase(whisperToasts_.begin());
|
|
|
|
|
|
whisperToasts_.push_back(std::move(toast));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
whisperSeenCount_ = histSize;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 22:00:30 -07:00
|
|
|
|
int chatMsgIdx = 0;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
for (const auto& msg : chatHistory) {
|
2026-02-14 14:30:09 -08:00
|
|
|
|
if (!shouldShowMessage(msg, activeChatTab_)) continue;
|
2026-02-19 02:27:01 -08:00
|
|
|
|
std::string processedMessage = replaceGenderPlaceholders(msg.message, gameHandler);
|
2026-02-14 14:30:09 -08:00
|
|
|
|
|
2026-03-10 15:18:00 -07:00
|
|
|
|
// Resolve sender name at render time in case it wasn't available at parse time.
|
|
|
|
|
|
// This handles the race where SMSG_MESSAGECHAT arrives before the entity spawns.
|
|
|
|
|
|
const std::string& resolvedSenderName = [&]() -> const std::string& {
|
|
|
|
|
|
if (!msg.senderName.empty()) return msg.senderName;
|
|
|
|
|
|
if (msg.senderGuid == 0) return msg.senderName;
|
|
|
|
|
|
const std::string& cached = gameHandler.lookupName(msg.senderGuid);
|
|
|
|
|
|
if (!cached.empty()) return cached;
|
|
|
|
|
|
return msg.senderName;
|
|
|
|
|
|
}();
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
ImVec4 color = getChatTypeColor(msg.type);
|
|
|
|
|
|
|
2026-02-14 18:27:59 -08:00
|
|
|
|
// Optional timestamp prefix
|
|
|
|
|
|
std::string tsPrefix;
|
|
|
|
|
|
if (chatShowTimestamps_) {
|
|
|
|
|
|
auto tt = std::chrono::system_clock::to_time_t(msg.timestamp);
|
|
|
|
|
|
std::tm tm{};
|
2026-02-18 18:50:27 -08:00
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
|
localtime_s(&tm, &tt);
|
|
|
|
|
|
#else
|
2026-02-14 18:27:59 -08:00
|
|
|
|
localtime_r(&tt, &tm);
|
2026-02-18 18:50:27 -08:00
|
|
|
|
#endif
|
2026-02-14 18:27:59 -08:00
|
|
|
|
char tsBuf[16];
|
|
|
|
|
|
snprintf(tsBuf, sizeof(tsBuf), "[%02d:%02d] ", tm.tm_hour, tm.tm_min);
|
|
|
|
|
|
tsPrefix = tsBuf;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 15:09:41 -07:00
|
|
|
|
// Build chat tag prefix: <GM>, <AFK>, <DND> from chatTag bitmask
|
|
|
|
|
|
std::string tagPrefix;
|
|
|
|
|
|
if (msg.chatTag & 0x04) tagPrefix = "<GM> ";
|
|
|
|
|
|
else if (msg.chatTag & 0x01) tagPrefix = "<AFK> ";
|
|
|
|
|
|
else if (msg.chatTag & 0x02) tagPrefix = "<DND> ";
|
|
|
|
|
|
|
2026-03-11 22:00:30 -07:00
|
|
|
|
// Build full message string for this entry
|
|
|
|
|
|
std::string fullMsg;
|
|
|
|
|
|
if (msg.type == game::ChatType::SYSTEM || msg.type == game::ChatType::TEXT_EMOTE) {
|
|
|
|
|
|
fullMsg = tsPrefix + processedMessage;
|
2026-03-10 15:18:00 -07:00
|
|
|
|
} else if (!resolvedSenderName.empty()) {
|
2026-03-10 14:59:02 -07:00
|
|
|
|
if (msg.type == game::ChatType::SAY ||
|
|
|
|
|
|
msg.type == game::ChatType::MONSTER_SAY || msg.type == game::ChatType::MONSTER_PARTY) {
|
2026-03-11 22:00:30 -07:00
|
|
|
|
fullMsg = tsPrefix + tagPrefix + resolvedSenderName + " says: " + processedMessage;
|
2026-03-10 14:59:02 -07:00
|
|
|
|
} else if (msg.type == game::ChatType::YELL || msg.type == game::ChatType::MONSTER_YELL) {
|
2026-03-11 22:00:30 -07:00
|
|
|
|
fullMsg = tsPrefix + tagPrefix + resolvedSenderName + " yells: " + processedMessage;
|
2026-03-10 14:59:02 -07:00
|
|
|
|
} else if (msg.type == game::ChatType::WHISPER ||
|
|
|
|
|
|
msg.type == game::ChatType::MONSTER_WHISPER || msg.type == game::ChatType::RAID_BOSS_WHISPER) {
|
2026-03-11 22:00:30 -07:00
|
|
|
|
fullMsg = tsPrefix + tagPrefix + resolvedSenderName + " whispers: " + processedMessage;
|
2026-03-10 15:08:21 -07:00
|
|
|
|
} else if (msg.type == game::ChatType::WHISPER_INFORM) {
|
2026-03-10 15:18:00 -07:00
|
|
|
|
const std::string& target = !msg.receiverName.empty() ? msg.receiverName : resolvedSenderName;
|
2026-03-11 22:00:30 -07:00
|
|
|
|
fullMsg = tsPrefix + "To " + target + ": " + processedMessage;
|
2026-03-10 14:59:02 -07:00
|
|
|
|
} else if (msg.type == game::ChatType::EMOTE ||
|
|
|
|
|
|
msg.type == game::ChatType::MONSTER_EMOTE || msg.type == game::ChatType::RAID_BOSS_EMOTE) {
|
2026-03-11 22:00:30 -07:00
|
|
|
|
fullMsg = tsPrefix + tagPrefix + resolvedSenderName + " " + processedMessage;
|
2026-02-14 18:27:59 -08:00
|
|
|
|
} else if (msg.type == game::ChatType::CHANNEL && !msg.channelName.empty()) {
|
|
|
|
|
|
int chIdx = gameHandler.getChannelIndex(msg.channelName);
|
|
|
|
|
|
std::string chDisplay = chIdx > 0
|
|
|
|
|
|
? "[" + std::to_string(chIdx) + ". " + msg.channelName + "]"
|
|
|
|
|
|
: "[" + msg.channelName + "]";
|
2026-03-11 22:00:30 -07:00
|
|
|
|
fullMsg = tsPrefix + chDisplay + " [" + tagPrefix + resolvedSenderName + "]: " + processedMessage;
|
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
|
|
|
|
} else {
|
2026-03-11 22:00:30 -07:00
|
|
|
|
fullMsg = tsPrefix + "[" + std::string(getChatTypeName(msg.type)) + "] " + tagPrefix + resolvedSenderName + ": " + processedMessage;
|
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-02-02 12:24:50 -08:00
|
|
|
|
} else {
|
2026-03-10 15:32:04 -07:00
|
|
|
|
bool isGroupType =
|
|
|
|
|
|
msg.type == game::ChatType::PARTY ||
|
|
|
|
|
|
msg.type == game::ChatType::GUILD ||
|
|
|
|
|
|
msg.type == game::ChatType::OFFICER ||
|
|
|
|
|
|
msg.type == game::ChatType::RAID ||
|
|
|
|
|
|
msg.type == game::ChatType::RAID_LEADER ||
|
|
|
|
|
|
msg.type == game::ChatType::RAID_WARNING ||
|
|
|
|
|
|
msg.type == game::ChatType::BATTLEGROUND ||
|
|
|
|
|
|
msg.type == game::ChatType::BATTLEGROUND_LEADER;
|
|
|
|
|
|
if (isGroupType) {
|
2026-03-11 22:00:30 -07:00
|
|
|
|
fullMsg = tsPrefix + "[" + std::string(getChatTypeName(msg.type)) + "] " + processedMessage;
|
2026-03-10 15:32:04 -07:00
|
|
|
|
} else {
|
2026-03-11 22:00:30 -07:00
|
|
|
|
fullMsg = tsPrefix + processedMessage;
|
2026-03-10 15:32:04 -07:00
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
2026-03-11 22:00:30 -07:00
|
|
|
|
|
2026-03-12 06:45:27 -07:00
|
|
|
|
// Detect mention: does this message contain the local player's name?
|
|
|
|
|
|
bool isMention = false;
|
|
|
|
|
|
if (!selfNameLower.empty() &&
|
|
|
|
|
|
msg.type != game::ChatType::WHISPER_INFORM &&
|
|
|
|
|
|
msg.type != game::ChatType::SYSTEM) {
|
|
|
|
|
|
std::string msgLower = fullMsg;
|
|
|
|
|
|
for (auto& c : msgLower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
isMention = (msgLower.find(selfNameLower) != std::string::npos);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 22:00:30 -07:00
|
|
|
|
// Render message in a group so we can attach a right-click context menu
|
|
|
|
|
|
ImGui::PushID(chatMsgIdx++);
|
2026-03-12 06:45:27 -07:00
|
|
|
|
if (isMention) {
|
|
|
|
|
|
// Golden highlight strip behind the text
|
|
|
|
|
|
ImVec2 groupMin = ImGui::GetCursorScreenPos();
|
|
|
|
|
|
float availW = ImGui::GetContentRegionAvail().x;
|
|
|
|
|
|
float lineH = ImGui::GetTextLineHeightWithSpacing();
|
|
|
|
|
|
ImGui::GetWindowDrawList()->AddRectFilled(
|
|
|
|
|
|
groupMin,
|
|
|
|
|
|
ImVec2(groupMin.x + availW, groupMin.y + lineH),
|
|
|
|
|
|
IM_COL32(255, 200, 50, 45)); // soft golden tint
|
|
|
|
|
|
}
|
2026-03-11 22:00:30 -07:00
|
|
|
|
ImGui::BeginGroup();
|
2026-03-12 06:45:27 -07:00
|
|
|
|
renderTextWithLinks(fullMsg, isMention ? ImVec4(1.0f, 0.9f, 0.35f, 1.0f) : color);
|
2026-03-11 22:00:30 -07:00
|
|
|
|
ImGui::EndGroup();
|
|
|
|
|
|
|
|
|
|
|
|
// Right-click context menu (only for player messages with a sender)
|
|
|
|
|
|
bool isPlayerMsg = !resolvedSenderName.empty() &&
|
|
|
|
|
|
msg.type != game::ChatType::SYSTEM &&
|
|
|
|
|
|
msg.type != game::ChatType::TEXT_EMOTE &&
|
|
|
|
|
|
msg.type != game::ChatType::MONSTER_SAY &&
|
|
|
|
|
|
msg.type != game::ChatType::MONSTER_YELL &&
|
|
|
|
|
|
msg.type != game::ChatType::MONSTER_WHISPER &&
|
|
|
|
|
|
msg.type != game::ChatType::MONSTER_EMOTE &&
|
|
|
|
|
|
msg.type != game::ChatType::MONSTER_PARTY &&
|
|
|
|
|
|
msg.type != game::ChatType::RAID_BOSS_WHISPER &&
|
|
|
|
|
|
msg.type != game::ChatType::RAID_BOSS_EMOTE;
|
|
|
|
|
|
|
|
|
|
|
|
if (isPlayerMsg && ImGui::BeginPopupContextItem("ChatMsgCtx")) {
|
|
|
|
|
|
ImGui::TextDisabled("%s", resolvedSenderName.c_str());
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Whisper")) {
|
|
|
|
|
|
selectedChatType = 4; // WHISPER
|
|
|
|
|
|
strncpy(whisperTargetBuffer, resolvedSenderName.c_str(), sizeof(whisperTargetBuffer) - 1);
|
|
|
|
|
|
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
2026-03-11 23:49:37 -07:00
|
|
|
|
if (ImGui::MenuItem("Invite to Group")) {
|
|
|
|
|
|
gameHandler.inviteToGroup(resolvedSenderName);
|
|
|
|
|
|
}
|
2026-03-11 22:00:30 -07:00
|
|
|
|
if (ImGui::MenuItem("Add Friend")) {
|
|
|
|
|
|
gameHandler.addFriend(resolvedSenderName);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Ignore")) {
|
|
|
|
|
|
gameHandler.addIgnore(resolvedSenderName);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 23:24:27 -07:00
|
|
|
|
// Auto-scroll to bottom; track whether user has scrolled up
|
|
|
|
|
|
{
|
|
|
|
|
|
float scrollY = ImGui::GetScrollY();
|
|
|
|
|
|
float scrollMaxY = ImGui::GetScrollMaxY();
|
|
|
|
|
|
bool atBottom = (scrollMaxY <= 0.0f) || (scrollY >= scrollMaxY - 2.0f);
|
|
|
|
|
|
if (atBottom || chatForceScrollToBottom_) {
|
|
|
|
|
|
ImGui::SetScrollHereY(1.0f);
|
|
|
|
|
|
chatScrolledUp_ = false;
|
|
|
|
|
|
chatForceScrollToBottom_ = false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
chatScrolledUp_ = true;
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
|
2026-02-14 18:27:59 -08:00
|
|
|
|
// Reset font scale after chat history
|
|
|
|
|
|
ImGui::SetWindowFontScale(1.0f);
|
|
|
|
|
|
|
2026-03-11 23:24:27 -07:00
|
|
|
|
// "Jump to bottom" indicator when scrolled up
|
|
|
|
|
|
if (chatScrolledUp_) {
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.35f, 0.7f, 0.9f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.5f, 0.9f, 1.0f));
|
|
|
|
|
|
if (ImGui::SmallButton(" v New messages ")) {
|
|
|
|
|
|
chatForceScrollToBottom_ = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::Spacing();
|
2026-02-06 18:34:45 -08:00
|
|
|
|
// Lock toggle
|
|
|
|
|
|
ImGui::Checkbox("Lock", &chatWindowLocked);
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextDisabled(chatWindowLocked ? "(locked)" : "(movable)");
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
// Chat input
|
|
|
|
|
|
ImGui::Text("Type:");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::SetNextItemWidth(100);
|
2026-03-12 11:16:42 -07:00
|
|
|
|
const char* chatTypes[] = { "SAY", "YELL", "PARTY", "GUILD", "WHISPER", "RAID", "OFFICER", "BATTLEGROUND", "RAID WARNING", "INSTANCE", "CHANNEL" };
|
|
|
|
|
|
ImGui::Combo("##ChatType", &selectedChatType, chatTypes, 11);
|
2026-02-07 12:30:36 -08:00
|
|
|
|
|
|
|
|
|
|
// Auto-fill whisper target when switching to WHISPER mode
|
|
|
|
|
|
if (selectedChatType == 4 && lastChatType != 4) {
|
|
|
|
|
|
// Just switched to WHISPER mode
|
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
|
auto target = gameHandler.getTarget();
|
|
|
|
|
|
if (target && target->getType() == game::ObjectType::PLAYER) {
|
|
|
|
|
|
auto player = std::static_pointer_cast<game::Player>(target);
|
|
|
|
|
|
if (!player->getName().empty()) {
|
|
|
|
|
|
strncpy(whisperTargetBuffer, player->getName().c_str(), sizeof(whisperTargetBuffer) - 1);
|
|
|
|
|
|
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
lastChatType = selectedChatType;
|
|
|
|
|
|
|
|
|
|
|
|
// Show whisper target field if WHISPER is selected
|
|
|
|
|
|
if (selectedChatType == 4) {
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::Text("To:");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::SetNextItemWidth(120);
|
|
|
|
|
|
ImGui::InputText("##WhisperTarget", whisperTargetBuffer, sizeof(whisperTargetBuffer));
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-03-12 11:16:42 -07:00
|
|
|
|
// Show channel picker if CHANNEL is selected
|
|
|
|
|
|
if (selectedChatType == 10) {
|
|
|
|
|
|
const auto& channels = gameHandler.getJoinedChannels();
|
|
|
|
|
|
if (channels.empty()) {
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextDisabled("(no channels joined)");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (selectedChannelIdx >= static_cast<int>(channels.size())) selectedChannelIdx = 0;
|
|
|
|
|
|
ImGui::SetNextItemWidth(140);
|
|
|
|
|
|
if (ImGui::BeginCombo("##ChannelPicker", channels[selectedChannelIdx].c_str())) {
|
|
|
|
|
|
for (int ci = 0; ci < static_cast<int>(channels.size()); ++ci) {
|
|
|
|
|
|
bool selected = (ci == selectedChannelIdx);
|
|
|
|
|
|
if (ImGui::Selectable(channels[ci].c_str(), selected)) selectedChannelIdx = ci;
|
|
|
|
|
|
if (selected) ImGui::SetItemDefaultFocus();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndCombo();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::Text("Message:");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextItemWidth(-1);
|
|
|
|
|
|
if (refocusChatInput) {
|
|
|
|
|
|
ImGui::SetKeyboardFocusHere();
|
|
|
|
|
|
refocusChatInput = false;
|
|
|
|
|
|
}
|
2026-02-07 23:32:27 -08:00
|
|
|
|
|
|
|
|
|
|
// Detect chat channel prefix as user types and switch the dropdown
|
|
|
|
|
|
{
|
|
|
|
|
|
std::string buf(chatInputBuffer);
|
|
|
|
|
|
if (buf.size() >= 2 && buf[0] == '/') {
|
|
|
|
|
|
// Find the command and check if there's a space after it
|
|
|
|
|
|
size_t sp = buf.find(' ', 1);
|
|
|
|
|
|
if (sp != std::string::npos) {
|
|
|
|
|
|
std::string cmd = buf.substr(1, sp - 1);
|
|
|
|
|
|
for (char& c : cmd) c = std::tolower(c);
|
|
|
|
|
|
int detected = -1;
|
2026-03-17 10:12:49 -07:00
|
|
|
|
bool isReply = false;
|
2026-02-07 23:32:27 -08:00
|
|
|
|
if (cmd == "s" || cmd == "say") detected = 0;
|
|
|
|
|
|
else if (cmd == "y" || cmd == "yell" || cmd == "shout") detected = 1;
|
|
|
|
|
|
else if (cmd == "p" || cmd == "party") detected = 2;
|
|
|
|
|
|
else if (cmd == "g" || cmd == "guild") detected = 3;
|
|
|
|
|
|
else if (cmd == "w" || cmd == "whisper" || cmd == "tell" || cmd == "t") detected = 4;
|
2026-03-17 10:12:49 -07:00
|
|
|
|
else if (cmd == "r" || cmd == "reply") { detected = 4; isReply = true; }
|
2026-02-07 23:32:27 -08:00
|
|
|
|
else if (cmd == "raid" || cmd == "rsay" || cmd == "ra") detected = 5;
|
|
|
|
|
|
else if (cmd == "o" || cmd == "officer" || cmd == "osay") detected = 6;
|
|
|
|
|
|
else if (cmd == "bg" || cmd == "battleground") detected = 7;
|
|
|
|
|
|
else if (cmd == "rw" || cmd == "raidwarning") detected = 8;
|
|
|
|
|
|
else if (cmd == "i" || cmd == "instance") detected = 9;
|
2026-03-12 11:16:42 -07:00
|
|
|
|
else if (cmd.size() == 1 && cmd[0] >= '1' && cmd[0] <= '9') detected = 10; // /1, /2 etc.
|
2026-03-17 10:12:49 -07:00
|
|
|
|
if (detected >= 0 && (selectedChatType != detected || detected == 10 || isReply)) {
|
2026-03-12 11:16:42 -07:00
|
|
|
|
// For channel shortcuts, also update selectedChannelIdx
|
|
|
|
|
|
if (detected == 10) {
|
|
|
|
|
|
int chanIdx = cmd[0] - '1'; // /1 -> index 0, /2 -> index 1, etc.
|
|
|
|
|
|
const auto& chans = gameHandler.getJoinedChannels();
|
|
|
|
|
|
if (chanIdx >= 0 && chanIdx < static_cast<int>(chans.size())) {
|
|
|
|
|
|
selectedChannelIdx = chanIdx;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-07 23:32:27 -08:00
|
|
|
|
selectedChatType = detected;
|
|
|
|
|
|
// Strip the prefix, keep only the message part
|
|
|
|
|
|
std::string remaining = buf.substr(sp + 1);
|
2026-03-17 10:12:49 -07:00
|
|
|
|
// /r reply: pre-fill whisper target from last whisper sender
|
|
|
|
|
|
if (detected == 4 && isReply) {
|
|
|
|
|
|
std::string lastSender = gameHandler.getLastWhisperSender();
|
|
|
|
|
|
if (!lastSender.empty()) {
|
|
|
|
|
|
strncpy(whisperTargetBuffer, lastSender.c_str(), sizeof(whisperTargetBuffer) - 1);
|
|
|
|
|
|
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
|
|
|
|
|
}
|
|
|
|
|
|
// remaining is the message — don't extract a target from it
|
|
|
|
|
|
} else if (detected == 4) {
|
|
|
|
|
|
// For whisper, first word after /w is the target
|
2026-02-07 23:32:27 -08:00
|
|
|
|
size_t msgStart = remaining.find(' ');
|
|
|
|
|
|
if (msgStart != std::string::npos) {
|
|
|
|
|
|
std::string wTarget = remaining.substr(0, msgStart);
|
|
|
|
|
|
strncpy(whisperTargetBuffer, wTarget.c_str(), sizeof(whisperTargetBuffer) - 1);
|
|
|
|
|
|
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
|
|
|
|
|
remaining = remaining.substr(msgStart + 1);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Just the target name so far, no message yet
|
|
|
|
|
|
strncpy(whisperTargetBuffer, remaining.c_str(), sizeof(whisperTargetBuffer) - 1);
|
|
|
|
|
|
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
|
|
|
|
|
remaining = "";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
strncpy(chatInputBuffer, remaining.c_str(), sizeof(chatInputBuffer) - 1);
|
|
|
|
|
|
chatInputBuffer[sizeof(chatInputBuffer) - 1] = '\0';
|
|
|
|
|
|
chatInputMoveCursorToEnd = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Color the input text based on current chat type
|
|
|
|
|
|
ImVec4 inputColor;
|
|
|
|
|
|
switch (selectedChatType) {
|
|
|
|
|
|
case 1: inputColor = ImVec4(1.0f, 0.3f, 0.3f, 1.0f); break; // YELL - red
|
|
|
|
|
|
case 2: inputColor = ImVec4(0.4f, 0.6f, 1.0f, 1.0f); break; // PARTY - blue
|
|
|
|
|
|
case 3: inputColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); break; // GUILD - green
|
|
|
|
|
|
case 4: inputColor = ImVec4(1.0f, 0.5f, 1.0f, 1.0f); break; // WHISPER - pink
|
|
|
|
|
|
case 5: inputColor = ImVec4(1.0f, 0.5f, 0.0f, 1.0f); break; // RAID - orange
|
|
|
|
|
|
case 6: inputColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); break; // OFFICER - green
|
|
|
|
|
|
case 7: inputColor = ImVec4(1.0f, 0.5f, 0.0f, 1.0f); break; // BG - orange
|
|
|
|
|
|
case 8: inputColor = ImVec4(1.0f, 0.3f, 0.0f, 1.0f); break; // RAID WARNING - red-orange
|
2026-03-12 11:16:42 -07:00
|
|
|
|
case 9: inputColor = ImVec4(0.4f, 0.6f, 1.0f, 1.0f); break; // INSTANCE - blue
|
|
|
|
|
|
case 10: inputColor = ImVec4(0.3f, 0.9f, 0.9f, 1.0f); break; // CHANNEL - cyan
|
2026-02-07 23:32:27 -08:00
|
|
|
|
default: inputColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); break; // SAY - white
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, inputColor);
|
|
|
|
|
|
|
2026-02-06 18:34:45 -08:00
|
|
|
|
auto inputCallback = [](ImGuiInputTextCallbackData* data) -> int {
|
|
|
|
|
|
auto* self = static_cast<GameScreen*>(data->UserData);
|
2026-03-11 23:06:24 -07:00
|
|
|
|
if (!self) return 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Cursor-to-end after channel switch
|
|
|
|
|
|
if (self->chatInputMoveCursorToEnd) {
|
2026-02-06 18:34:45 -08:00
|
|
|
|
int len = static_cast<int>(std::strlen(data->Buf));
|
|
|
|
|
|
data->CursorPos = len;
|
|
|
|
|
|
data->SelectionStart = len;
|
|
|
|
|
|
data->SelectionEnd = len;
|
|
|
|
|
|
self->chatInputMoveCursorToEnd = false;
|
|
|
|
|
|
}
|
2026-03-11 23:06:24 -07:00
|
|
|
|
|
2026-03-12 06:38:10 -07:00
|
|
|
|
// Tab: slash-command autocomplete
|
|
|
|
|
|
if (data->EventFlag == ImGuiInputTextFlags_CallbackCompletion) {
|
|
|
|
|
|
if (data->BufTextLen > 0 && data->Buf[0] == '/') {
|
|
|
|
|
|
// Split buffer into command word and trailing args
|
|
|
|
|
|
std::string fullBuf(data->Buf, data->BufTextLen);
|
|
|
|
|
|
size_t spacePos = fullBuf.find(' ');
|
|
|
|
|
|
std::string word = (spacePos != std::string::npos) ? fullBuf.substr(0, spacePos) : fullBuf;
|
|
|
|
|
|
std::string rest = (spacePos != std::string::npos) ? fullBuf.substr(spacePos) : "";
|
|
|
|
|
|
|
|
|
|
|
|
// Normalize to lowercase for matching
|
|
|
|
|
|
std::string lowerWord = word;
|
|
|
|
|
|
for (auto& ch : lowerWord) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
|
|
|
|
|
|
|
|
|
|
|
static const std::vector<std::string> kCmds = {
|
|
|
|
|
|
"/afk", "/away", "/cast", "/chathelp", "/clear",
|
|
|
|
|
|
"/dance", "/do", "/dnd", "/e", "/emote",
|
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
|
|
|
|
"/cl", "/combatlog", "/equip", "/follow", "/g", "/guild", "/guildinfo",
|
2026-03-12 06:38:10 -07:00
|
|
|
|
"/gmticket", "/grouploot", "/i", "/instance",
|
|
|
|
|
|
"/invite", "/j", "/join", "/kick",
|
|
|
|
|
|
"/l", "/leave", "/local", "/me",
|
|
|
|
|
|
"/p", "/party", "/r", "/raid",
|
|
|
|
|
|
"/raidwarning", "/random", "/reply", "/roll",
|
|
|
|
|
|
"/s", "/say", "/setloot", "/shout",
|
|
|
|
|
|
"/stopattack", "/stopfollow", "/t", "/time",
|
2026-03-12 10:06:11 -07:00
|
|
|
|
"/trade", "/uninvite", "/use", "/w", "/whisper",
|
2026-03-12 06:38:10 -07:00
|
|
|
|
"/who", "/wts", "/wtb", "/y", "/yell", "/zone"
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// New session if prefix changed
|
|
|
|
|
|
if (self->chatTabMatchIdx_ < 0 || self->chatTabPrefix_ != lowerWord) {
|
|
|
|
|
|
self->chatTabPrefix_ = lowerWord;
|
|
|
|
|
|
self->chatTabMatches_.clear();
|
|
|
|
|
|
for (const auto& cmd : kCmds) {
|
|
|
|
|
|
if (cmd.size() >= lowerWord.size() &&
|
|
|
|
|
|
cmd.compare(0, lowerWord.size(), lowerWord) == 0)
|
|
|
|
|
|
self->chatTabMatches_.push_back(cmd);
|
|
|
|
|
|
}
|
|
|
|
|
|
self->chatTabMatchIdx_ = 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Cycle forward through matches
|
|
|
|
|
|
++self->chatTabMatchIdx_;
|
|
|
|
|
|
if (self->chatTabMatchIdx_ >= static_cast<int>(self->chatTabMatches_.size()))
|
|
|
|
|
|
self->chatTabMatchIdx_ = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!self->chatTabMatches_.empty()) {
|
|
|
|
|
|
std::string match = self->chatTabMatches_[self->chatTabMatchIdx_];
|
|
|
|
|
|
// Append trailing space when match is unambiguous
|
|
|
|
|
|
if (self->chatTabMatches_.size() == 1 && rest.empty())
|
|
|
|
|
|
match += ' ';
|
|
|
|
|
|
std::string newBuf = match + rest;
|
|
|
|
|
|
data->DeleteChars(0, data->BufTextLen);
|
|
|
|
|
|
data->InsertChars(0, newBuf.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 23:06:24 -07:00
|
|
|
|
// Up/Down arrow: cycle through sent message history
|
|
|
|
|
|
if (data->EventFlag == ImGuiInputTextFlags_CallbackHistory) {
|
2026-03-12 06:38:10 -07:00
|
|
|
|
// Any history navigation resets autocomplete
|
|
|
|
|
|
self->chatTabMatchIdx_ = -1;
|
|
|
|
|
|
self->chatTabMatches_.clear();
|
|
|
|
|
|
|
2026-03-11 23:06:24 -07:00
|
|
|
|
const int histSize = static_cast<int>(self->chatSentHistory_.size());
|
|
|
|
|
|
if (histSize == 0) return 0;
|
|
|
|
|
|
|
|
|
|
|
|
if (data->EventKey == ImGuiKey_UpArrow) {
|
|
|
|
|
|
// Go back in history
|
|
|
|
|
|
if (self->chatHistoryIdx_ == -1)
|
|
|
|
|
|
self->chatHistoryIdx_ = histSize - 1;
|
|
|
|
|
|
else if (self->chatHistoryIdx_ > 0)
|
|
|
|
|
|
--self->chatHistoryIdx_;
|
|
|
|
|
|
} else if (data->EventKey == ImGuiKey_DownArrow) {
|
|
|
|
|
|
if (self->chatHistoryIdx_ == -1) return 0;
|
|
|
|
|
|
++self->chatHistoryIdx_;
|
|
|
|
|
|
if (self->chatHistoryIdx_ >= histSize) {
|
|
|
|
|
|
self->chatHistoryIdx_ = -1;
|
|
|
|
|
|
data->DeleteChars(0, data->BufTextLen);
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (self->chatHistoryIdx_ >= 0 && self->chatHistoryIdx_ < histSize) {
|
|
|
|
|
|
const std::string& entry = self->chatSentHistory_[self->chatHistoryIdx_];
|
|
|
|
|
|
data->DeleteChars(0, data->BufTextLen);
|
|
|
|
|
|
data->InsertChars(0, entry.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-06 18:34:45 -08:00
|
|
|
|
return 0;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-11 23:06:24 -07:00
|
|
|
|
ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags_EnterReturnsTrue |
|
|
|
|
|
|
ImGuiInputTextFlags_CallbackAlways |
|
2026-03-12 06:38:10 -07:00
|
|
|
|
ImGuiInputTextFlags_CallbackHistory |
|
|
|
|
|
|
ImGuiInputTextFlags_CallbackCompletion;
|
2026-02-06 18:34:45 -08:00
|
|
|
|
if (ImGui::InputText("##ChatInput", chatInputBuffer, sizeof(chatInputBuffer), inputFlags, inputCallback, this)) {
|
2026-02-02 12:24:50 -08:00
|
|
|
|
sendChatMessage(gameHandler);
|
2026-02-14 22:00:26 -08:00
|
|
|
|
// Close chat input on send so movement keys work immediately.
|
|
|
|
|
|
refocusChatInput = false;
|
|
|
|
|
|
ImGui::ClearActiveID();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
2026-02-07 23:32:27 -08:00
|
|
|
|
ImGui::PopStyleColor();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
if (ImGui::IsItemActive()) {
|
|
|
|
|
|
chatInputActive = true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
chatInputActive = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 21:12:54 -08:00
|
|
|
|
// Click in chat history area (received messages) → focus input.
|
2026-02-07 21:00:05 -08:00
|
|
|
|
{
|
2026-02-07 21:12:54 -08:00
|
|
|
|
if (chatHistoryHovered && ImGui::IsMouseClicked(0)) {
|
|
|
|
|
|
refocusChatInput = true;
|
2026-02-07 21:00:05 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|
|
|
|
|
auto& io = ImGui::GetIO();
|
|
|
|
|
|
auto& input = core::Input::getInstance();
|
2026-03-14 03:54:26 -07:00
|
|
|
|
|
|
|
|
|
|
// 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 && !chatInputActive && input.isKeyJustPressed(SDL_SCANCODE_SLASH)) {
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
chatInputBuffer[0] = '/';
|
|
|
|
|
|
chatInputBuffer[1] = '\0';
|
|
|
|
|
|
chatInputMoveCursorToEnd = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!io.WantTextInput && !chatInputActive &&
|
|
|
|
|
|
KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHAT, true)) {
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 05:20:23 -07:00
|
|
|
|
const bool textFocus = chatInputActive || refocusChatInput || io.WantTextInput;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
// Tab targeting (when keyboard not captured by UI)
|
|
|
|
|
|
if (!io.WantCaptureKeyboard) {
|
2026-03-14 03:49:42 -07:00
|
|
|
|
// When typing in chat (or any text input), never treat keys as gameplay/UI shortcuts.
|
|
|
|
|
|
if (!textFocus && input.isKeyJustPressed(SDL_SCANCODE_TAB)) {
|
2026-02-02 12:24:50 -08:00
|
|
|
|
const auto& movement = gameHandler.getMovementInfo();
|
|
|
|
|
|
gameHandler.tabTarget(movement.x, movement.y, movement.z);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 09:08:15 -07:00
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_SETTINGS, true)) {
|
2026-02-09 17:12:35 -08:00
|
|
|
|
if (showSettingsWindow) {
|
|
|
|
|
|
// Close settings window if open
|
|
|
|
|
|
showSettingsWindow = false;
|
|
|
|
|
|
} else if (showEscapeMenu) {
|
2026-02-05 16:01:38 -08:00
|
|
|
|
showEscapeMenu = false;
|
|
|
|
|
|
showEscapeSettingsNotice = false;
|
2026-02-04 18:27:52 -08:00
|
|
|
|
} else if (gameHandler.isCasting()) {
|
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.cancelCast();
|
|
|
|
|
|
} else if (gameHandler.isLootWindowOpen()) {
|
|
|
|
|
|
gameHandler.closeLoot();
|
|
|
|
|
|
} else if (gameHandler.isGossipWindowOpen()) {
|
|
|
|
|
|
gameHandler.closeGossip();
|
|
|
|
|
|
} else {
|
2026-02-05 16:01:38 -08:00
|
|
|
|
showEscapeMenu = true;
|
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-14 03:49:42 -07:00
|
|
|
|
if (!textFocus) {
|
|
|
|
|
|
// Toggle character screen (C) and inventory/bags (I)
|
|
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHARACTER_SCREEN)) {
|
2026-03-14 07:29:39 -07:00
|
|
|
|
const bool wasOpen = inventoryScreen.isCharacterOpen();
|
2026-03-14 03:49:42 -07:00
|
|
|
|
inventoryScreen.toggleCharacter();
|
2026-03-14 07:29:39 -07:00
|
|
|
|
if (!wasOpen && gameHandler.isConnected()) {
|
|
|
|
|
|
gameHandler.requestPlayedTime();
|
|
|
|
|
|
}
|
2026-03-14 03:49:42 -07:00
|
|
|
|
}
|
2026-03-11 09:05:17 -07:00
|
|
|
|
|
2026-03-14 03:49:42 -07:00
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_INVENTORY)) {
|
2026-03-12 12:05:05 -07:00
|
|
|
|
inventoryScreen.toggle();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 03:49:42 -07:00
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_NAMEPLATES)) {
|
|
|
|
|
|
showNameplates_ = !showNameplates_;
|
|
|
|
|
|
}
|
2026-03-11 07:38:08 -07:00
|
|
|
|
|
2026-03-14 03:49:42 -07:00
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_WORLD_MAP)) {
|
|
|
|
|
|
showWorldMap_ = !showWorldMap_;
|
|
|
|
|
|
}
|
2026-03-11 09:24:37 -07:00
|
|
|
|
|
2026-03-14 03:49:42 -07:00
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_MINIMAP)) {
|
|
|
|
|
|
showMinimap_ = !showMinimap_;
|
|
|
|
|
|
}
|
2026-03-11 09:24:37 -07:00
|
|
|
|
|
2026-03-14 03:49:42 -07:00
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_RAID_FRAMES)) {
|
|
|
|
|
|
showRaidFrames_ = !showRaidFrames_;
|
|
|
|
|
|
}
|
2026-03-12 00:05:55 -07:00
|
|
|
|
|
2026-03-14 03:49:42 -07:00
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_ACHIEVEMENTS)) {
|
|
|
|
|
|
showAchievementWindow_ = !showAchievementWindow_;
|
|
|
|
|
|
}
|
2026-03-17 20:46:41 -07:00
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_SKILLS)) {
|
|
|
|
|
|
showSkillsWindow_ = !showSkillsWindow_;
|
|
|
|
|
|
}
|
2026-03-12 20:23:36 -07:00
|
|
|
|
|
2026-03-14 03:49:42 -07: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-12 20:28:03 -07:00
|
|
|
|
|
2026-03-14 03:49:42 -07: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
|
|
|
|
|
|
};
|
|
|
|
|
|
const bool shiftDown = input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT);
|
2026-03-17 15:18:04 -07:00
|
|
|
|
const bool ctrlDown = input.isKeyPressed(SDL_SCANCODE_LCTRL) || input.isKeyPressed(SDL_SCANCODE_RCTRL);
|
2026-03-14 03:49:42 -07:00
|
|
|
|
const auto& bar = gameHandler.getActionBar();
|
2026-03-17 15:18:04 -07: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;
|
|
|
|
|
|
}
|
|
|
|
|
|
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-14 03:49:42 -07:00
|
|
|
|
for (int i = 0; i < game::GameHandler::SLOTS_PER_BAR; ++i) {
|
2026-03-17 15:18:04 -07:00
|
|
|
|
if (!ctrlDown && input.isKeyJustPressed(actionBarKeys[i])) {
|
2026-03-14 03:49:42 -07:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
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-02-05 16:14:11 -08:00
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-19 03:12:57 -08:00
|
|
|
|
// Cursor affordance: show hand cursor over interactable game objects.
|
|
|
|
|
|
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 hoverInteractableGo = false;
|
|
|
|
|
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
|
|
|
|
|
if (entity->getType() != game::ObjectType::GAMEOBJECT) continue;
|
|
|
|
|
|
|
|
|
|
|
|
glm::vec3 hitCenter;
|
|
|
|
|
|
float hitRadius = 0.0f;
|
|
|
|
|
|
bool hasBounds = core::Application::getInstance().getRenderBoundsForGuid(guid, hitCenter, hitRadius);
|
|
|
|
|
|
if (!hasBounds) {
|
|
|
|
|
|
hitRadius = 2.5f;
|
|
|
|
|
|
hitCenter = core::coords::canonicalToRender(glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
|
|
|
|
|
hitCenter.z += 1.2f;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
hitRadius = std::max(hitRadius * 1.1f, 0.8f);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
float hitT;
|
|
|
|
|
|
if (raySphereIntersect(ray, hitCenter, hitRadius, hitT) && hitT < closestT) {
|
|
|
|
|
|
closestT = hitT;
|
|
|
|
|
|
hoverInteractableGo = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (hoverInteractableGo) {
|
|
|
|
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 13:53:03 -08:00
|
|
|
|
// Left-click targeting: only on mouse-up if the mouse didn't drag (camera rotate)
|
|
|
|
|
|
// Record press position on mouse-down
|
2026-02-06 11:59:51 -08:00
|
|
|
|
if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_LEFT) && !input.isMouseButtonPressed(SDL_BUTTON_RIGHT)) {
|
2026-02-07 13:53:03 -08:00
|
|
|
|
leftClickPressPos_ = input.getMousePosition();
|
|
|
|
|
|
leftClickWasPress_ = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
|
float dragDist = glm::length(releasePos - leftClickPressPos_);
|
|
|
|
|
|
constexpr float CLICK_THRESHOLD = 5.0f; // pixels
|
|
|
|
|
|
|
|
|
|
|
|
if (dragDist < CLICK_THRESHOLD) {
|
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
|
|
|
|
|
|
if (camera && window) {
|
|
|
|
|
|
float screenW = static_cast<float>(window->getWidth());
|
|
|
|
|
|
float screenH = static_cast<float>(window->getHeight());
|
|
|
|
|
|
|
|
|
|
|
|
rendering::Ray ray = camera->screenToWorldRay(leftClickPressPos_.x, leftClickPressPos_.y, screenW, screenH);
|
|
|
|
|
|
|
|
|
|
|
|
float closestT = 1e30f;
|
|
|
|
|
|
uint64_t closestGuid = 0;
|
2026-02-20 20:37:55 -08:00
|
|
|
|
float closestHostileUnitT = 1e30f;
|
|
|
|
|
|
uint64_t closestHostileUnitGuid = 0;
|
2026-02-07 13:53:03 -08:00
|
|
|
|
|
|
|
|
|
|
const uint64_t myGuid = gameHandler.getPlayerGuid();
|
|
|
|
|
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
|
|
|
|
|
auto t = entity->getType();
|
2026-02-07 19:44:03 -08:00
|
|
|
|
if (t != game::ObjectType::UNIT &&
|
2026-02-20 19:51:04 -08:00
|
|
|
|
t != game::ObjectType::PLAYER &&
|
|
|
|
|
|
t != game::ObjectType::GAMEOBJECT) continue;
|
2026-02-07 13:53:03 -08:00
|
|
|
|
if (guid == myGuid) continue; // Don't target self
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-02-20 19:51:04 -08:00
|
|
|
|
} else if (t == game::ObjectType::GAMEOBJECT) {
|
|
|
|
|
|
hitRadius = 2.5f;
|
|
|
|
|
|
heightOffset = 1.2f;
|
2026-02-06 18:34:45 -08:00
|
|
|
|
}
|
2026-02-07 13:53:03 -08: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-02-06 13:47:03 -08:00
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-07 13:53:03 -08:00
|
|
|
|
float hitT;
|
|
|
|
|
|
if (raySphereIntersect(ray, hitCenter, hitRadius, hitT)) {
|
2026-02-20 20:37:55 -08:00
|
|
|
|
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-02-07 13:53:03 -08:00
|
|
|
|
if (hitT < closestT) {
|
|
|
|
|
|
closestT = hitT;
|
|
|
|
|
|
closestGuid = guid;
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-20 20:37:55 -08:00
|
|
|
|
// Prefer hostile monsters over nearby gameobjects/others when both are hittable.
|
|
|
|
|
|
if (closestHostileUnitGuid != 0) {
|
|
|
|
|
|
closestGuid = closestHostileUnitGuid;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 13:53:03 -08:00
|
|
|
|
if (closestGuid != 0) {
|
|
|
|
|
|
gameHandler.setTarget(closestGuid);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Clicked empty space — deselect current target
|
|
|
|
|
|
gameHandler.clearTarget();
|
|
|
|
|
|
}
|
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
|
|
|
|
|
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
|
|
|
|
// Right-click: select NPC (if needed) then interact / loot / auto-attack
|
2026-02-06 11:59:51 -08:00
|
|
|
|
// Suppress when left button is held (both-button run)
|
|
|
|
|
|
if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_RIGHT) && !input.isMouseButtonPressed(SDL_BUTTON_LEFT)) {
|
2026-02-20 19:51:04 -08:00
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
// 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) {
|
2026-03-14 04:33:46 -07:00
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
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;
|
2026-02-19 03:12:57 -08:00
|
|
|
|
game::ObjectType closestType = game::ObjectType::OBJECT;
|
2026-02-20 20:37:55 -08:00
|
|
|
|
float closestHostileUnitT = 1e30f;
|
|
|
|
|
|
uint64_t closestHostileUnitGuid = 0;
|
2026-03-14 04:33:46 -07:00
|
|
|
|
float closestQuestGoT = 1e30f;
|
|
|
|
|
|
uint64_t closestQuestGoGuid = 0;
|
2026-03-17 10:12:49 -07:00
|
|
|
|
float closestGoT = 1e30f;
|
|
|
|
|
|
uint64_t closestGoGuid = 0;
|
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
|
|
|
|
const uint64_t myGuid = gameHandler.getPlayerGuid();
|
|
|
|
|
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
|
|
|
|
|
auto t = entity->getType();
|
2026-02-07 19:44:03 -08:00
|
|
|
|
if (t != game::ObjectType::UNIT &&
|
|
|
|
|
|
t != game::ObjectType::PLAYER &&
|
2026-03-14 04:33:46 -07:00
|
|
|
|
t != game::ObjectType::GAMEOBJECT)
|
|
|
|
|
|
continue;
|
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
|
|
|
|
if (guid == myGuid) continue;
|
2026-03-14 04:33:46 -07:00
|
|
|
|
|
2026-02-06 18:34:45 -08: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;
|
|
|
|
|
|
}
|
2026-02-07 19:44:03 -08:00
|
|
|
|
} else if (t == game::ObjectType::GAMEOBJECT) {
|
2026-03-17 10:12:49 -07:00
|
|
|
|
hitRadius = 2.5f;
|
|
|
|
|
|
heightOffset = 1.2f;
|
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-02-06 18:34:45 -08: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);
|
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-14 04:33:46 -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
|
|
|
|
float hitT;
|
2026-02-06 18:34:45 -08:00
|
|
|
|
if (raySphereIntersect(ray, hitCenter, hitRadius, hitT)) {
|
2026-02-20 20:37:55 -08:00
|
|
|
|
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-17 10:12:49 -07:00
|
|
|
|
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-14 04:33:46 -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
|
|
|
|
if (hitT < closestT) {
|
|
|
|
|
|
closestT = hitT;
|
|
|
|
|
|
closestGuid = guid;
|
2026-02-19 03:12:57 -08:00
|
|
|
|
closestType = t;
|
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-14 04:33:46 -07:00
|
|
|
|
|
2026-03-17 10:12:49 -07:00
|
|
|
|
// Priority: quest GO > closer of (GO, hostile unit) > closest anything.
|
2026-03-14 04:33:46 -07:00
|
|
|
|
if (closestQuestGoGuid != 0) {
|
|
|
|
|
|
closestGuid = closestQuestGoGuid;
|
|
|
|
|
|
closestType = game::ObjectType::GAMEOBJECT;
|
2026-03-17 10:12:49 -07:00
|
|
|
|
} 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;
|
2026-03-14 04:33:46 -07:00
|
|
|
|
} else if (closestHostileUnitGuid != 0) {
|
2026-02-20 20:37:55 -08:00
|
|
|
|
closestGuid = closestHostileUnitGuid;
|
|
|
|
|
|
closestType = game::ObjectType::UNIT;
|
|
|
|
|
|
}
|
2026-03-14 04:33:46 -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
|
|
|
|
if (closestGuid != 0) {
|
2026-02-19 03:12:57 -08:00
|
|
|
|
if (closestType == game::ObjectType::GAMEOBJECT) {
|
2026-02-20 19:51:04 -08:00
|
|
|
|
gameHandler.setTarget(closestGuid);
|
2026-02-19 03:12:57 -08:00
|
|
|
|
gameHandler.interactWithGameObject(closestGuid);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
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
|
|
|
|
gameHandler.setTarget(closestGuid);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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
|
|
|
|
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 {
|
2026-02-20 16:27:21 -08:00
|
|
|
|
// Interact with service NPCs; otherwise treat non-interactable living units
|
|
|
|
|
|
// as attackable fallback (covers bad faction-template classification).
|
2026-02-07 21:00:05 -08:00
|
|
|
|
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);
|
|
|
|
|
|
};
|
2026-02-07 23:12:24 -08:00
|
|
|
|
bool allowSpiritInteract = (gameHandler.isPlayerDead() || gameHandler.isPlayerGhost()) && isSpiritNpc();
|
2026-02-20 16:27:21 -08:00
|
|
|
|
bool canInteractNpc = unit->isInteractable() || allowSpiritInteract;
|
|
|
|
|
|
bool shouldAttackByFallback = !canInteractNpc;
|
|
|
|
|
|
if (!unit->isHostile() && canInteractNpc) {
|
2026-02-06 03:11:43 -08:00
|
|
|
|
gameHandler.interactWithNpc(target->getGuid());
|
2026-02-20 16:27:21 -08:00
|
|
|
|
} else if (unit->isHostile() || shouldAttackByFallback) {
|
2026-02-17 05:27:03 -08:00
|
|
|
|
gameHandler.startAutoAttack(target->getGuid());
|
2026-02-06 03:11:43 -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
|
|
|
|
}
|
2026-02-07 19:44:03 -08:00
|
|
|
|
} else if (target->getType() == game::ObjectType::GAMEOBJECT) {
|
|
|
|
|
|
gameHandler.interactWithGameObject(target->getGuid());
|
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 if (target->getType() == game::ObjectType::PLAYER) {
|
|
|
|
|
|
// Right-click another player could start attack in PvP context
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) {
|
2026-02-06 18:34:45 -08:00
|
|
|
|
bool isDead = gameHandler.isPlayerDead();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(10.0f, 30.0f), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(250.0f, 0.0f), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.85f));
|
2026-02-20 03:38:12 -08:00
|
|
|
|
const bool inCombatConfirmed = gameHandler.isInCombat();
|
|
|
|
|
|
const bool attackIntentOnly = gameHandler.hasAutoAttackIntent() && !inCombatConfirmed;
|
2026-02-06 18:34:45 -08:00
|
|
|
|
ImVec4 playerBorder = isDead
|
|
|
|
|
|
? ImVec4(0.5f, 0.5f, 0.5f, 1.0f)
|
2026-02-20 03:38:12 -08:00
|
|
|
|
: (inCombatConfirmed
|
2026-02-06 18:34:45 -08:00
|
|
|
|
? ImVec4(1.0f, 0.2f, 0.2f, 1.0f)
|
2026-02-20 03:38:12 -08:00
|
|
|
|
: (attackIntentOnly
|
|
|
|
|
|
? ImVec4(1.0f, 0.7f, 0.2f, 1.0f)
|
|
|
|
|
|
: ImVec4(0.4f, 0.4f, 0.4f, 1.0f)));
|
2026-02-06 18:34:45 -08:00
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Border, playerBorder);
|
2026-02-02 12:24:50 -08: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;
|
|
|
|
|
|
|
|
|
|
|
|
const auto& characters = gameHandler.getCharacters();
|
2026-02-06 21:25:35 -08:00
|
|
|
|
uint64_t activeGuid = gameHandler.getActiveCharacterGuid();
|
|
|
|
|
|
const game::Character* activeChar = nullptr;
|
|
|
|
|
|
for (const auto& c : characters) {
|
|
|
|
|
|
if (c.guid == activeGuid) { activeChar = &c; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!activeChar && !characters.empty()) activeChar = &characters[0];
|
|
|
|
|
|
if (activeChar) {
|
|
|
|
|
|
const auto& ch = *activeChar;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
playerName = ch.name;
|
2026-02-06 13:47:03 -08:00
|
|
|
|
// Use live server level if available, otherwise character struct
|
|
|
|
|
|
playerLevel = gameHandler.getPlayerLevel();
|
|
|
|
|
|
if (playerLevel == 0) playerLevel = ch.level;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
playerMaxHp = 20 + playerLevel * 10;
|
|
|
|
|
|
playerHp = playerMaxHp;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 08:35:47 -07:00
|
|
|
|
// Derive class color via shared helper
|
|
|
|
|
|
ImVec4 classColor = activeChar
|
|
|
|
|
|
? classColorVec4(static_cast<uint8_t>(activeChar->characterClass))
|
|
|
|
|
|
: ImVec4(0.3f, 1.0f, 0.3f, 1.0f);
|
2026-03-12 04:39:38 -07:00
|
|
|
|
|
|
|
|
|
|
// Name in class color — clickable for self-target, right-click for menu
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, classColor);
|
2026-02-06 17:27:20 -08:00
|
|
|
|
if (ImGui::Selectable(playerName.c_str(), false, 0, ImVec2(0, 0))) {
|
|
|
|
|
|
gameHandler.setTarget(gameHandler.getPlayerGuid());
|
|
|
|
|
|
}
|
2026-03-11 22:36:58 -07:00
|
|
|
|
if (ImGui::BeginPopupContextItem("PlayerSelfCtx")) {
|
2026-03-12 00:03:23 -07:00
|
|
|
|
ImGui::TextDisabled("%s", playerName.c_str());
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Open Character")) {
|
2026-03-11 22:36:58 -07:00
|
|
|
|
inventoryScreen.setCharacterOpen(true);
|
|
|
|
|
|
}
|
2026-03-12 00:03:23 -07:00
|
|
|
|
if (ImGui::MenuItem("Toggle PvP")) {
|
2026-03-11 22:36:58 -07:00
|
|
|
|
gameHandler.togglePvp();
|
|
|
|
|
|
}
|
2026-03-12 00:10:54 -07:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
2026-03-12 00:03:23 -07:00
|
|
|
|
if (gameHandler.isInGroup()) {
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Leave Group")) {
|
|
|
|
|
|
gameHandler.leaveGroup();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-11 22:36:58 -07:00
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
2026-02-06 17:27:20 -08:00
|
|
|
|
ImGui::PopStyleColor();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextDisabled("Lv %u", playerLevel);
|
2026-02-06 18:34:45 -08:00
|
|
|
|
if (isDead) {
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.9f, 0.2f, 0.2f, 1.0f), "DEAD");
|
|
|
|
|
|
}
|
2026-03-12 14:47:51 -07:00
|
|
|
|
// 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(ImVec4(1.0f, 0.85f, 0.1f, 1.0f), "\xe2\x99\x9b");
|
|
|
|
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("You are the group leader");
|
|
|
|
|
|
}
|
2026-03-11 23:21:27 -07:00
|
|
|
|
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");
|
|
|
|
|
|
}
|
2026-03-12 05:28:47 -07: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-02-02 12:24:50 -08:00
|
|
|
|
|
2026-03-13 09:10:03 -07: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());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
// 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 04:06:46 -07:00
|
|
|
|
// Health bar — color transitions green→yellow→red as HP drops
|
2026-02-02 12:24:50 -08:00
|
|
|
|
float pct = static_cast<float>(playerHp) / static_cast<float>(playerMaxHp);
|
2026-03-12 04:06:46 -07:00
|
|
|
|
ImVec4 hpColor;
|
|
|
|
|
|
if (isDead) {
|
|
|
|
|
|
hpColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
|
|
|
|
|
|
} else if (pct > 0.5f) {
|
|
|
|
|
|
hpColor = ImVec4(0.2f, 0.8f, 0.2f, 1.0f); // 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
|
|
|
|
|
|
}
|
2026-02-06 18:34:45 -08:00
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, hpColor);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
char overlay[64];
|
|
|
|
|
|
snprintf(overlay, sizeof(overlay), "%u / %u", playerHp, playerMaxHp);
|
|
|
|
|
|
ImGui::ProgressBar(pct, ImVec2(-1, 18), overlay);
|
|
|
|
|
|
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-02-19 17:08:53 -08:00
|
|
|
|
// Mana/Power bar
|
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
|
|
|
|
if (playerEntity && (playerEntity->getType() == game::ObjectType::PLAYER || playerEntity->getType() == game::ObjectType::UNIT)) {
|
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(playerEntity);
|
2026-02-19 17:08:53 -08:00
|
|
|
|
uint8_t powerType = unit->getPowerType();
|
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
|
|
|
|
uint32_t power = unit->getPower();
|
|
|
|
|
|
uint32_t maxPower = unit->getMaxPower();
|
2026-03-09 18:18:07 -07:00
|
|
|
|
// 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;
|
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
|
|
|
|
if (maxPower > 0) {
|
|
|
|
|
|
float mpPct = static_cast<float>(power) / static_cast<float>(maxPower);
|
|
|
|
|
|
ImVec4 powerColor;
|
2026-02-19 17:08:53 -08:00
|
|
|
|
switch (powerType) {
|
2026-03-12 04:18:39 -07:00
|
|
|
|
case 0: {
|
|
|
|
|
|
// Mana: pulse desaturated blue when critically low (< 20%)
|
|
|
|
|
|
if (mpPct < 0.2f) {
|
|
|
|
|
|
float pulse = 0.6f + 0.4f * std::sin(static_cast<float>(ImGui::GetTime()) * 3.0f);
|
|
|
|
|
|
powerColor = ImVec4(0.1f, 0.1f, 0.8f * pulse, 1.0f);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
powerColor = ImVec4(0.2f, 0.2f, 0.9f, 1.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
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
|
|
|
|
case 1: powerColor = ImVec4(0.9f, 0.2f, 0.2f, 1.0f); break; // Rage (red)
|
2026-03-09 17:09:48 -07:00
|
|
|
|
case 2: powerColor = ImVec4(0.9f, 0.6f, 0.1f, 1.0f); break; // Focus (orange)
|
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
|
|
|
|
case 3: powerColor = ImVec4(0.9f, 0.9f, 0.2f, 1.0f); break; // Energy (yellow)
|
2026-03-09 17:09:48 -07:00
|
|
|
|
case 4: powerColor = ImVec4(0.5f, 0.9f, 0.3f, 1.0f); break; // Happiness (green)
|
|
|
|
|
|
case 6: powerColor = ImVec4(0.8f, 0.1f, 0.2f, 1.0f); break; // Runic Power (crimson)
|
|
|
|
|
|
case 7: powerColor = ImVec4(0.4f, 0.1f, 0.6f, 1.0f); break; // Soul Shards (purple)
|
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
|
|
|
|
default: powerColor = ImVec4(0.2f, 0.2f, 0.9f, 1.0f); break;
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, powerColor);
|
|
|
|
|
|
char mpOverlay[64];
|
|
|
|
|
|
snprintf(mpOverlay, sizeof(mpOverlay), "%u / %u", power, maxPower);
|
2026-02-19 17:08:53 -08:00
|
|
|
|
ImGui::ProgressBar(mpPct, ImVec2(-1, 18), mpOverlay);
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-09 18:28:03 -07: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;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImVec2 cursor = ImGui::GetCursorScreenPos();
|
|
|
|
|
|
float totalW = ImGui::GetContentRegionAvail().x;
|
|
|
|
|
|
float spacing = 3.0f;
|
|
|
|
|
|
float squareW = (totalW - spacing * 5.0f) / 6.0f;
|
|
|
|
|
|
float squareH = 14.0f;
|
|
|
|
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < 6; i++) {
|
|
|
|
|
|
// Client-side prediction: advance fill over ~10s cooldown
|
|
|
|
|
|
runeClientFill_[i] = runes[i].ready ? 1.0f
|
|
|
|
|
|
: std::min(runeClientFill_[i] + dt / 10.0f, runes[i].readyFraction + 0.02f);
|
|
|
|
|
|
runeClientFill_[i] = std::clamp(runeClientFill_[i], 0.0f, runes[i].ready ? 1.0f : 0.97f);
|
|
|
|
|
|
|
|
|
|
|
|
float x0 = cursor.x + i * (squareW + spacing);
|
|
|
|
|
|
float y0 = cursor.y;
|
|
|
|
|
|
float x1 = x0 + squareW;
|
|
|
|
|
|
float y1 = y0 + squareH;
|
|
|
|
|
|
|
|
|
|
|
|
// Background (dark)
|
|
|
|
|
|
dl->AddRectFilled(ImVec2(x0, y0), ImVec2(x1, y1),
|
|
|
|
|
|
IM_COL32(30, 30, 30, 200), 2.0f);
|
|
|
|
|
|
|
|
|
|
|
|
// Fill color by rune type
|
|
|
|
|
|
ImVec4 fc;
|
|
|
|
|
|
switch (runes[i].type) {
|
|
|
|
|
|
case game::GameHandler::RuneType::Blood: fc = ImVec4(0.85f, 0.12f, 0.12f, 1.0f); break;
|
|
|
|
|
|
case game::GameHandler::RuneType::Unholy: fc = ImVec4(0.20f, 0.72f, 0.20f, 1.0f); break;
|
|
|
|
|
|
case game::GameHandler::RuneType::Frost: fc = ImVec4(0.30f, 0.55f, 0.90f, 1.0f); break;
|
|
|
|
|
|
case game::GameHandler::RuneType::Death: fc = ImVec4(0.55f, 0.20f, 0.70f, 1.0f); break;
|
|
|
|
|
|
default: fc = ImVec4(0.6f, 0.6f, 0.6f, 1.0f); break;
|
|
|
|
|
|
}
|
|
|
|
|
|
float fillX = x0 + (x1 - x0) * runeClientFill_[i];
|
|
|
|
|
|
dl->AddRectFilled(ImVec2(x0, y0), ImVec2(fillX, y1),
|
|
|
|
|
|
ImGui::ColorConvertFloat4ToU32(fc), 2.0f);
|
|
|
|
|
|
|
|
|
|
|
|
// Border
|
|
|
|
|
|
ImU32 borderCol = runes[i].ready
|
|
|
|
|
|
? IM_COL32(220, 220, 220, 180)
|
|
|
|
|
|
: IM_COL32(100, 100, 100, 160);
|
|
|
|
|
|
dl->AddRect(ImVec2(x0, y0), ImVec2(x1, y1), borderCol, 2.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Dummy(ImVec2(totalW, squareH));
|
|
|
|
|
|
}
|
2026-03-09 23:09:58 -07:00
|
|
|
|
|
|
|
|
|
|
// Combo point display — Rogue (4) and Druid (11) in Cat Form
|
|
|
|
|
|
{
|
|
|
|
|
|
uint8_t cls = gameHandler.getPlayerClass();
|
|
|
|
|
|
const bool isRogue = (cls == 4);
|
|
|
|
|
|
const bool isDruid = (cls == 11);
|
|
|
|
|
|
if (isRogue || isDruid) {
|
|
|
|
|
|
uint8_t cp = gameHandler.getComboPoints();
|
|
|
|
|
|
if (cp > 0 || isRogue) { // always show for rogue; only when non-zero for druid
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImVec2 cursor = ImGui::GetCursorScreenPos();
|
|
|
|
|
|
float totalW = ImGui::GetContentRegionAvail().x;
|
|
|
|
|
|
constexpr int MAX_CP = 5;
|
|
|
|
|
|
constexpr float DOT_R = 7.0f;
|
|
|
|
|
|
constexpr float SPACING = 4.0f;
|
|
|
|
|
|
float totalDotsW = MAX_CP * (DOT_R * 2.0f) + (MAX_CP - 1) * SPACING;
|
|
|
|
|
|
float startX = cursor.x + (totalW - totalDotsW) * 0.5f;
|
|
|
|
|
|
float cy = cursor.y + DOT_R;
|
|
|
|
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
|
|
|
|
for (int i = 0; i < MAX_CP; ++i) {
|
|
|
|
|
|
float cx = startX + i * (DOT_R * 2.0f + SPACING) + DOT_R;
|
|
|
|
|
|
ImU32 col = (i < static_cast<int>(cp))
|
|
|
|
|
|
? IM_COL32(255, 210, 0, 240) // bright gold — active
|
|
|
|
|
|
: IM_COL32(60, 60, 60, 160); // dark — empty
|
|
|
|
|
|
dl->AddCircleFilled(ImVec2(cx, cy), DOT_R, col);
|
|
|
|
|
|
dl->AddCircle(ImVec2(cx, cy), DOT_R, IM_COL32(160, 140, 0, 180), 0, 1.5f);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Dummy(ImVec2(totalW, DOT_R * 2.0f));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 05:16:43 -07:00
|
|
|
|
|
|
|
|
|
|
// Shaman totem bar (class 7) — 4 slots: Earth, Fire, Water, Air
|
|
|
|
|
|
if (gameHandler.getPlayerClass() == 7) {
|
|
|
|
|
|
static const 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 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[8];
|
|
|
|
|
|
snprintf(secBuf, sizeof(secBuf), "%.0f", rem / 1000.0f);
|
|
|
|
|
|
ImVec2 tsz = ImGui::CalcTextSize(secBuf);
|
|
|
|
|
|
float lx = x0 + (slotW - tsz.x) * 0.5f;
|
|
|
|
|
|
float ly = y0 + (slotH - tsz.y) * 0.5f;
|
|
|
|
|
|
tdl->AddText(ImVec2(lx + 1, ly + 1), IM_COL32(0, 0, 0, 180), secBuf);
|
|
|
|
|
|
tdl->AddText(ImVec2(lx, ly), IM_COL32(255, 255, 255, 230), secBuf);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Inactive — show element letter
|
|
|
|
|
|
const char* letter = kTotemNames[i];
|
|
|
|
|
|
char single[2] = { letter[0], '\0' };
|
|
|
|
|
|
ImVec2 tsz = ImGui::CalcTextSize(single);
|
|
|
|
|
|
float lx = x0 + (slotW - tsz.x) * 0.5f;
|
|
|
|
|
|
float ly = y0 + (slotH - tsz.y) * 0.5f;
|
|
|
|
|
|
tdl->AddText(ImVec2(lx, ly), IM_COL32(80, 80, 80, 200), single);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Border
|
|
|
|
|
|
ImU32 borderCol = ts.active()
|
|
|
|
|
|
? ImGui::ColorConvertFloat4ToU32(kTotemColors[i])
|
|
|
|
|
|
: IM_COL32(60, 60, 60, 160);
|
|
|
|
|
|
tdl->AddRect(ImVec2(x0, y0), ImVec2(x1, y1), borderCol, 2.0f);
|
|
|
|
|
|
|
|
|
|
|
|
// Tooltip on hover
|
|
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(x0, y0));
|
|
|
|
|
|
ImGui::InvisibleButton(("##totem" + std::to_string(i)).c_str(), ImVec2(slotW, slotH));
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
if (ts.active()) {
|
|
|
|
|
|
const std::string& spellNm = gameHandler.getSpellName(ts.spellId);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(kTotemColors[i].x, kTotemColors[i].y,
|
|
|
|
|
|
kTotemColors[i].z, 1.0f),
|
|
|
|
|
|
"%s Totem", kTotemNames[i]);
|
|
|
|
|
|
if (!spellNm.empty()) ImGui::Text("%s", spellNm.c_str());
|
|
|
|
|
|
ImGui::Text("%.1fs remaining", ts.remainingMs() / 1000.0f);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextDisabled("%s Totem (empty)", kTotemNames[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cursor.x, cursor.y + slotH + 2.0f));
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
2026-03-12 20:05:36 -07: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;
|
|
|
|
|
|
|
|
|
|
|
|
// 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-02-02 12:24:50 -08:00
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
2026-03-09 17:23:28 -07:00
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderPetFrame(game::GameHandler& gameHandler) {
|
|
|
|
|
|
uint64_t petGuid = gameHandler.getPetGuid();
|
|
|
|
|
|
if (petGuid == 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto petEntity = gameHandler.getEntityManager().getEntity(petGuid);
|
|
|
|
|
|
if (!petEntity) return;
|
|
|
|
|
|
auto* petUnit = dynamic_cast<game::Unit*>(petEntity.get());
|
|
|
|
|
|
if (!petUnit) return;
|
|
|
|
|
|
|
2026-03-09 17:25:46 -07: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);
|
2026-03-09 17:23:28 -07:00
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(200.0f, 0.0f), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.1f, 0.08f, 0.85f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.2f, 0.6f, 0.2f, 1.0f));
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##PetFrame", nullptr, flags)) {
|
|
|
|
|
|
const std::string& petName = petUnit->getName();
|
|
|
|
|
|
uint32_t petLevel = petUnit->getLevel();
|
|
|
|
|
|
|
|
|
|
|
|
// Name + level on one row — clicking the pet name targets it
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.9f, 0.4f, 1.0f));
|
|
|
|
|
|
char petLabel[96];
|
|
|
|
|
|
snprintf(petLabel, sizeof(petLabel), "%s",
|
|
|
|
|
|
petName.empty() ? "Pet" : petName.c_str());
|
|
|
|
|
|
if (ImGui::Selectable(petLabel, false, 0, ImVec2(0, 0))) {
|
|
|
|
|
|
gameHandler.setTarget(petGuid);
|
|
|
|
|
|
}
|
2026-03-12 00:10:54 -07:00
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
2026-03-12 19:42:31 -07:00
|
|
|
|
if (ImGui::MenuItem("Rename Pet")) {
|
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
|
petRenameOpen_ = true;
|
|
|
|
|
|
petRenameBuf_[0] = '\0';
|
|
|
|
|
|
}
|
2026-03-12 00:10:54 -07:00
|
|
|
|
if (ImGui::MenuItem("Dismiss Pet")) {
|
|
|
|
|
|
gameHandler.dismissPet();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
2026-03-12 19:42:31 -07:00
|
|
|
|
// 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();
|
|
|
|
|
|
}
|
2026-03-09 17:23:28 -07:00
|
|
|
|
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);
|
2026-03-12 04:06:46 -07:00
|
|
|
|
ImVec4 petHpColor = pct > 0.5f ? ImVec4(0.2f, 0.8f, 0.2f, 1.0f)
|
|
|
|
|
|
: 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);
|
2026-03-09 17:23:28 -07:00
|
|
|
|
char hpText[32];
|
|
|
|
|
|
snprintf(hpText, sizeof(hpText), "%u/%u", hp, maxHp);
|
|
|
|
|
|
ImGui::ProgressBar(pct, ImVec2(-1, 14), hpText);
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Power/mana bar (hunters' pets use focus)
|
|
|
|
|
|
uint8_t powerType = petUnit->getPowerType();
|
|
|
|
|
|
uint32_t power = petUnit->getPower();
|
|
|
|
|
|
uint32_t maxPower = petUnit->getMaxPower();
|
|
|
|
|
|
if (maxPower == 0 && (powerType == 1 || powerType == 2 || powerType == 3)) maxPower = 100;
|
|
|
|
|
|
if (maxPower > 0) {
|
|
|
|
|
|
float mpPct = static_cast<float>(power) / static_cast<float>(maxPower);
|
|
|
|
|
|
ImVec4 powerColor;
|
|
|
|
|
|
switch (powerType) {
|
|
|
|
|
|
case 0: powerColor = ImVec4(0.2f, 0.2f, 0.9f, 1.0f); break; // Mana
|
|
|
|
|
|
case 1: powerColor = ImVec4(0.9f, 0.2f, 0.2f, 1.0f); break; // Rage
|
|
|
|
|
|
case 2: powerColor = ImVec4(0.9f, 0.6f, 0.1f, 1.0f); break; // Focus (hunter pets)
|
|
|
|
|
|
case 3: powerColor = ImVec4(0.9f, 0.9f, 0.2f, 1.0f); break; // Energy
|
|
|
|
|
|
default: powerColor = ImVec4(0.2f, 0.2f, 0.9f, 1.0f); 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-12 09:43:23 -07: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-12 04:59:24 -07: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-12 09:05:28 -07:00
|
|
|
|
// Stance row: Passive / Defensive / Aggressive — with Dismiss right-aligned
|
|
|
|
|
|
{
|
|
|
|
|
|
static const char* kReactLabels[] = { "Psv", "Def", "Agg" };
|
|
|
|
|
|
static const char* kReactTooltips[] = { "Passive", "Defensive", "Aggressive" };
|
|
|
|
|
|
static const ImVec4 kReactColors[] = {
|
|
|
|
|
|
ImVec4(0.4f, 0.6f, 1.0f, 1.0f), // passive — blue
|
|
|
|
|
|
ImVec4(0.3f, 0.85f, 0.3f, 1.0f), // defensive — green
|
|
|
|
|
|
ImVec4(1.0f, 0.35f, 0.35f, 1.0f),// aggressive — red
|
|
|
|
|
|
};
|
|
|
|
|
|
static const 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();
|
|
|
|
|
|
}
|
2026-03-09 17:23:28 -07:00
|
|
|
|
}
|
2026-03-10 18:26:02 -07:00
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
|
|
|
|
uint32_t actionId = slotVal & 0x00FFFFFFu;
|
2026-03-10 18:37:05 -07:00
|
|
|
|
// Use the authoritative autocast set from SMSG_PET_SPELLS spell list flags.
|
|
|
|
|
|
bool autocastOn = gameHandler.isPetSpellAutocast(actionId);
|
2026-03-10 18:26:02 -07:00
|
|
|
|
|
2026-03-17 15:59:27 -07: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-10 18:26:02 -07:00
|
|
|
|
ImGui::PushID(i);
|
|
|
|
|
|
if (rendered > 0) ImGui::SameLine(0.0f, spacing);
|
|
|
|
|
|
|
|
|
|
|
|
// Try to show spell icon; fall back to abbreviated text label.
|
|
|
|
|
|
VkDescriptorSet iconTex = VK_NULL_HANDLE;
|
|
|
|
|
|
const char* builtinLabel = nullptr;
|
2026-03-12 09:05:28 -07:00
|
|
|
|
if (actionId == 1) builtinLabel = "Psv";
|
|
|
|
|
|
else if (actionId == 2) builtinLabel = "Fol";
|
2026-03-10 18:26:02 -07:00
|
|
|
|
else if (actionId == 3) builtinLabel = "Sty";
|
2026-03-12 09:05:28 -07:00
|
|
|
|
else if (actionId == 4) builtinLabel = "Def";
|
2026-03-10 18:26:02 -07:00
|
|
|
|
else if (actionId == 5) builtinLabel = "Atk";
|
2026-03-12 09:05:28 -07:00
|
|
|
|
else if (actionId == 6) builtinLabel = "Agg";
|
2026-03-10 18:26:02 -07:00
|
|
|
|
else if (assetMgr) iconTex = getSpellIcon(actionId, assetMgr);
|
|
|
|
|
|
|
2026-03-17 15:59:27 -07:00
|
|
|
|
// Dim when on cooldown; tint green when autocast is on
|
|
|
|
|
|
ImVec4 tint = petOnCd
|
|
|
|
|
|
? ImVec4(0.35f, 0.35f, 0.35f, 0.7f)
|
|
|
|
|
|
: (autocastOn ? ImVec4(0.6f, 1.0f, 0.6f, 1.0f) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
2026-03-10 18:26:02 -07:00
|
|
|
|
bool clicked = false;
|
|
|
|
|
|
if (iconTex) {
|
|
|
|
|
|
clicked = ImGui::ImageButton("##pa",
|
|
|
|
|
|
(ImTextureID)(uintptr_t)iconTex,
|
|
|
|
|
|
ImVec2(iconSz, iconSz),
|
|
|
|
|
|
ImVec2(0,0), ImVec2(1,1),
|
|
|
|
|
|
ImVec4(0.1f,0.1f,0.1f,0.9f), tint);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
char label[8];
|
|
|
|
|
|
if (builtinLabel) {
|
|
|
|
|
|
snprintf(label, sizeof(label), "%s", builtinLabel);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Show first 3 chars of spell name or spell ID.
|
|
|
|
|
|
std::string nm = gameHandler.getSpellName(actionId);
|
|
|
|
|
|
if (nm.empty()) snprintf(label, sizeof(label), "?%u", actionId % 100);
|
|
|
|
|
|
else snprintf(label, sizeof(label), "%.3s", nm.c_str());
|
|
|
|
|
|
}
|
2026-03-17 15:59:27 -07:00
|
|
|
|
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);
|
2026-03-10 18:26:02 -07:00
|
|
|
|
clicked = ImGui::Button(label, ImVec2(iconSz + 4.0f, iconSz));
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 15:59:27 -07: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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (clicked && !petOnCd) {
|
2026-03-10 18:26:02 -07:00
|
|
|
|
// Send pet action; use current target for spells.
|
|
|
|
|
|
uint64_t targetGuid = (actionId > 5) ? gameHandler.getTargetGuid() : 0u;
|
|
|
|
|
|
gameHandler.sendPetAction(slotVal, targetGuid);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 13:33:48 -07:00
|
|
|
|
// Tooltip: rich spell info for pet spells, simple label for built-in commands
|
2026-03-10 18:26:02 -07:00
|
|
|
|
if (ImGui::IsItemHovered()) {
|
2026-03-12 09:05:28 -07:00
|
|
|
|
if (builtinLabel) {
|
2026-03-12 13:33:48 -07:00
|
|
|
|
const char* tip = nullptr;
|
2026-03-12 09:05:28 -07:00
|
|
|
|
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";
|
2026-03-12 13:33:48 -07:00
|
|
|
|
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());
|
|
|
|
|
|
}
|
|
|
|
|
|
if (autocastOn)
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "Autocast: On");
|
2026-03-17 15:59:27 -07:00
|
|
|
|
if (petOnCd) {
|
|
|
|
|
|
if (petCd >= 60.0f)
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
|
|
|
|
|
|
"Cooldown: %d min %d sec",
|
|
|
|
|
|
static_cast<int>(petCd) / 60, static_cast<int>(petCd) % 60);
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
|
|
|
|
|
|
"Cooldown: %.1f sec", petCd);
|
|
|
|
|
|
}
|
2026-03-12 13:33:48 -07:00
|
|
|
|
ImGui::EndTooltip();
|
2026-03-12 09:05:28 -07:00
|
|
|
|
}
|
2026-03-10 18:26:02 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
++rendered;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-09 17:23:28 -07:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
ImGui::PopStyleColor(2);
|
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();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 10:14:44 -07:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Totem Frame (Shaman — below pet frame / player frame)
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderTotemFrame(game::GameHandler& gameHandler) {
|
|
|
|
|
|
// Only show if at least one totem is active
|
|
|
|
|
|
bool anyActive = false;
|
|
|
|
|
|
for (int i = 0; i < game::GameHandler::NUM_TOTEM_SLOTS; ++i) {
|
|
|
|
|
|
if (gameHandler.getTotemSlot(i).active()) { anyActive = true; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!anyActive) return;
|
|
|
|
|
|
|
|
|
|
|
|
static const struct { const char* name; ImU32 color; } kTotemInfo[4] = {
|
|
|
|
|
|
{ "Earth", IM_COL32(139, 90, 43, 255) }, // brown
|
|
|
|
|
|
{ "Fire", IM_COL32(220, 80, 30, 255) }, // red-orange
|
|
|
|
|
|
{ "Water", IM_COL32( 30,120, 220, 255) }, // blue
|
|
|
|
|
|
{ "Air", IM_COL32(180,220, 255, 255) }, // light blue
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Position: below pet frame / player frame, left side
|
|
|
|
|
|
// Pet frame is at ~y=200 if active, player frame is at y=20; totem frame near y=300
|
|
|
|
|
|
// We anchor relative to screen left edge like pet frame
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(8.0f, 300.0f), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(130.0f, 0.0f), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
|
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize |
|
|
|
|
|
|
ImGuiWindowFlags_NoTitleBar;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.08f, 0.06f, 0.88f));
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##TotemFrame", nullptr, flags)) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.9f, 0.75f, 0.3f, 1.0f), "Totems");
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < game::GameHandler::NUM_TOTEM_SLOTS; ++i) {
|
|
|
|
|
|
const auto& slot = gameHandler.getTotemSlot(i);
|
|
|
|
|
|
if (!slot.active()) continue;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushID(i);
|
|
|
|
|
|
|
|
|
|
|
|
// Colored element dot
|
|
|
|
|
|
ImVec2 dotPos = ImGui::GetCursorScreenPos();
|
|
|
|
|
|
dotPos.x += 4.0f; dotPos.y += 6.0f;
|
|
|
|
|
|
ImGui::GetWindowDrawList()->AddCircleFilled(
|
|
|
|
|
|
ImVec2(dotPos.x + 4.0f, dotPos.y + 4.0f), 4.0f, kTotemInfo[i].color);
|
|
|
|
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 14.0f);
|
|
|
|
|
|
|
|
|
|
|
|
// Totem name or spell name
|
|
|
|
|
|
const std::string& spellName = gameHandler.getSpellName(slot.spellId);
|
|
|
|
|
|
const char* displayName = spellName.empty() ? kTotemInfo[i].name : spellName.c_str();
|
|
|
|
|
|
ImGui::Text("%s", displayName);
|
|
|
|
|
|
|
|
|
|
|
|
// Duration countdown bar
|
|
|
|
|
|
float remMs = slot.remainingMs();
|
|
|
|
|
|
float totMs = static_cast<float>(slot.durationMs);
|
|
|
|
|
|
float frac = (totMs > 0.0f) ? std::min(remMs / totMs, 1.0f) : 0.0f;
|
|
|
|
|
|
float remSec = remMs / 1000.0f;
|
|
|
|
|
|
|
|
|
|
|
|
// Color bar with totem element tint
|
|
|
|
|
|
ImVec4 barCol(
|
|
|
|
|
|
static_cast<float>((kTotemInfo[i].color >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f,
|
|
|
|
|
|
static_cast<float>((kTotemInfo[i].color >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f,
|
|
|
|
|
|
static_cast<float>((kTotemInfo[i].color >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f,
|
|
|
|
|
|
0.9f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, barCol);
|
|
|
|
|
|
char timeBuf[16];
|
|
|
|
|
|
snprintf(timeBuf, sizeof(timeBuf), "%.0fs", remSec);
|
|
|
|
|
|
ImGui::ProgressBar(frac, ImVec2(-1, 8), timeBuf);
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) {
|
|
|
|
|
|
auto target = gameHandler.getTarget();
|
|
|
|
|
|
if (!target) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
|
|
|
|
|
|
float frameW = 250.0f;
|
|
|
|
|
|
float frameX = (screenW - frameW) / 2.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(frameX, 30.0f), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(frameW, 0.0f), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize;
|
|
|
|
|
|
|
2026-02-06 16:47:07 -08:00
|
|
|
|
// Determine hostility/level color for border and name (WoW-canonical)
|
2026-02-06 13:47:03 -08:00
|
|
|
|
ImVec4 hostileColor(0.7f, 0.7f, 0.7f, 1.0f);
|
|
|
|
|
|
if (target->getType() == game::ObjectType::PLAYER) {
|
|
|
|
|
|
hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f);
|
|
|
|
|
|
} else if (target->getType() == game::ObjectType::UNIT) {
|
|
|
|
|
|
auto u = std::static_pointer_cast<game::Unit>(target);
|
|
|
|
|
|
if (u->getHealth() == 0 && u->getMaxHealth() > 0) {
|
|
|
|
|
|
hostileColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
|
2026-02-06 14:24:38 -08:00
|
|
|
|
} else if (u->isHostile()) {
|
2026-02-06 16:47:07 -08:00
|
|
|
|
// WoW level-based color for hostile mobs
|
|
|
|
|
|
uint32_t playerLv = gameHandler.getPlayerLevel();
|
|
|
|
|
|
uint32_t mobLv = u->getLevel();
|
2026-03-17 14:16:14 -07:00
|
|
|
|
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);
|
2026-02-06 16:47:07 -08:00
|
|
|
|
} else {
|
2026-03-17 14:16:14 -07:00
|
|
|
|
int32_t diff = static_cast<int32_t>(mobLv) - static_cast<int32_t>(playerLv);
|
|
|
|
|
|
if (game::GameHandler::killXp(playerLv, mobLv) == 0) {
|
|
|
|
|
|
hostileColor = ImVec4(0.6f, 0.6f, 0.6f, 1.0f); // 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 = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Green - easy
|
|
|
|
|
|
}
|
2026-02-06 16:47:07 -08:00
|
|
|
|
}
|
2026-02-06 14:24:38 -08:00
|
|
|
|
} else {
|
2026-02-06 16:47:07 -08:00
|
|
|
|
hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Friendly
|
2026-02-06 13:47:03 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.85f));
|
2026-02-20 03:38:12 -08:00
|
|
|
|
const uint64_t targetGuid = target->getGuid();
|
|
|
|
|
|
const bool confirmedCombatWithTarget = gameHandler.isInCombatWith(targetGuid);
|
|
|
|
|
|
const bool intentTowardTarget =
|
|
|
|
|
|
gameHandler.hasAutoAttackIntent() &&
|
|
|
|
|
|
gameHandler.getAutoAttackTargetGuid() == targetGuid &&
|
|
|
|
|
|
!confirmedCombatWithTarget;
|
2026-02-06 18:34:45 -08:00
|
|
|
|
ImVec4 borderColor = ImVec4(hostileColor.x * 0.8f, hostileColor.y * 0.8f, hostileColor.z * 0.8f, 1.0f);
|
2026-02-20 03:38:12 -08:00
|
|
|
|
if (confirmedCombatWithTarget) {
|
2026-02-07 21:00:05 -08:00
|
|
|
|
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);
|
2026-02-20 03:38:12 -08:00
|
|
|
|
} else if (intentTowardTarget) {
|
|
|
|
|
|
borderColor = ImVec4(1.0f, 0.7f, 0.2f, 1.0f);
|
2026-02-06 18:34:45 -08:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Border, borderColor);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##TargetFrame", nullptr, flags)) {
|
2026-03-10 06:10:29 -07:00
|
|
|
|
// Raid mark icon (Star/Circle/Diamond/Triangle/Moon/Square/Cross/Skull)
|
|
|
|
|
|
static const 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 23:41:05 -07:00
|
|
|
|
// Entity name and type — Selectable so we can attach a right-click context menu
|
2026-02-02 12:24:50 -08:00
|
|
|
|
std::string name = getEntityName(target);
|
|
|
|
|
|
|
2026-03-12 08:33:34 -07:00
|
|
|
|
// Player targets: use class color instead of the generic green
|
2026-02-06 13:47:03 -08:00
|
|
|
|
ImVec4 nameColor = hostileColor;
|
2026-03-12 08:33:34 -07:00
|
|
|
|
if (target->getType() == game::ObjectType::PLAYER) {
|
|
|
|
|
|
uint8_t cid = entityClassId(target.get());
|
|
|
|
|
|
if (cid != 0) nameColor = classColorVec4(cid);
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-03-10 06:10:29 -07:00
|
|
|
|
ImGui::SameLine(0.0f, 0.0f);
|
2026-03-11 23:41:05 -07:00
|
|
|
|
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-03-12 14:39:29 -07: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(ImVec4(1.0f, 0.85f, 0.1f, 1.0f), "\xe2\x99\x9b");
|
|
|
|
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Group Leader");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:15:45 -07: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(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "!");
|
|
|
|
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Has a quest available");
|
|
|
|
|
|
} else if (qgs == QGS::AVAILABLE_LOW) {
|
|
|
|
|
|
ImGui::SameLine(0, 4);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "!");
|
|
|
|
|
|
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(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "?");
|
|
|
|
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Quest ready to turn in");
|
|
|
|
|
|
} else if (qgs == QGS::INCOMPLETE) {
|
|
|
|
|
|
ImGui::SameLine(0, 4);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "?");
|
|
|
|
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Quest incomplete");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:14:25 -07: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-03-11 23:41:05 -07: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();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TextDisabled("%s", name.c_str());
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::MenuItem("Set Focus")) {
|
|
|
|
|
|
gameHandler.setFocus(tGuid);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Clear Target")) {
|
|
|
|
|
|
gameHandler.clearTarget();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isPlayer) {
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Whisper")) {
|
|
|
|
|
|
selectedChatType = 4;
|
|
|
|
|
|
strncpy(whisperTargetBuffer, name.c_str(), sizeof(whisperTargetBuffer) - 1);
|
|
|
|
|
|
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
2026-03-11 23:58:37 -07:00
|
|
|
|
if (ImGui::MenuItem("Follow")) {
|
|
|
|
|
|
gameHandler.followTarget();
|
|
|
|
|
|
}
|
2026-03-11 23:41:05 -07:00
|
|
|
|
if (ImGui::MenuItem("Invite to Group")) {
|
|
|
|
|
|
gameHandler.inviteToGroup(name);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Trade")) {
|
|
|
|
|
|
gameHandler.initiateTrade(tGuid);
|
|
|
|
|
|
}
|
2026-03-11 23:56:57 -07:00
|
|
|
|
if (ImGui::MenuItem("Duel")) {
|
|
|
|
|
|
gameHandler.proposeDuel(tGuid);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Inspect")) {
|
|
|
|
|
|
gameHandler.inspectTarget();
|
2026-03-12 02:52:40 -07:00
|
|
|
|
showInspectWindow_ = true;
|
2026-03-11 23:56:57 -07:00
|
|
|
|
}
|
2026-03-11 23:41:05 -07:00
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Add Friend")) {
|
|
|
|
|
|
gameHandler.addFriend(name);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Ignore")) {
|
|
|
|
|
|
gameHandler.addIgnore(name);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 00:39:56 -07:00
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::BeginMenu("Set Raid Mark")) {
|
|
|
|
|
|
static const char* kRaidMarkNames[] = {
|
|
|
|
|
|
"{*} Star", "{O} Circle", "{<>} Diamond", "{^} Triangle",
|
|
|
|
|
|
"{)} Moon", "{ } Square", "{x} Cross", "{8} Skull"
|
|
|
|
|
|
};
|
|
|
|
|
|
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-03-11 23:41:05 -07:00
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-06 16:47:07 -08:00
|
|
|
|
// Level (for units/players) — colored by difficulty
|
2026-02-02 12:24:50 -08:00
|
|
|
|
if (target->getType() == game::ObjectType::UNIT || target->getType() == game::ObjectType::PLAYER) {
|
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(target);
|
|
|
|
|
|
ImGui::SameLine();
|
2026-02-06 16:47:07 -08:00
|
|
|
|
// Level color matches the hostility/difficulty color
|
|
|
|
|
|
ImVec4 levelColor = hostileColor;
|
|
|
|
|
|
if (target->getType() == game::ObjectType::PLAYER) {
|
|
|
|
|
|
levelColor = ImVec4(0.7f, 0.7f, 0.7f, 1.0f);
|
|
|
|
|
|
}
|
2026-03-17 14:16:14 -07:00
|
|
|
|
if (unit->getLevel() == 0)
|
|
|
|
|
|
ImGui::TextColored(levelColor, "Lv ??");
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::TextColored(levelColor, "Lv %u", unit->getLevel());
|
2026-03-12 14:13:09 -07:00
|
|
|
|
// 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(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "[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");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 05:28:47 -07:00
|
|
|
|
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");
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08: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 ? ImVec4(0.2f, 0.8f, 0.2f, 1.0f) :
|
|
|
|
|
|
pct > 0.2f ? ImVec4(0.8f, 0.8f, 0.2f, 1.0f) :
|
|
|
|
|
|
ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
|
|
|
|
|
|
|
|
|
|
|
|
char overlay[64];
|
|
|
|
|
|
snprintf(overlay, sizeof(overlay), "%u / %u", hp, maxHp);
|
|
|
|
|
|
ImGui::ProgressBar(pct, ImVec2(-1, 18), overlay);
|
|
|
|
|
|
ImGui::PopStyleColor();
|
2026-02-19 17:08:53 -08:00
|
|
|
|
// Target power bar (mana/rage/energy)
|
|
|
|
|
|
uint8_t targetPowerType = unit->getPowerType();
|
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
|
|
|
|
uint32_t targetPower = unit->getPower();
|
|
|
|
|
|
uint32_t targetMaxPower = unit->getMaxPower();
|
2026-02-19 17:08:53 -08:00
|
|
|
|
if (targetMaxPower == 0 && (targetPowerType == 1 || targetPowerType == 3)) targetMaxPower = 100;
|
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
|
|
|
|
if (targetMaxPower > 0) {
|
|
|
|
|
|
float mpPct = static_cast<float>(targetPower) / static_cast<float>(targetMaxPower);
|
2026-02-19 17:08:53 -08:00
|
|
|
|
ImVec4 targetPowerColor;
|
|
|
|
|
|
switch (targetPowerType) {
|
|
|
|
|
|
case 0: targetPowerColor = ImVec4(0.2f, 0.2f, 0.9f, 1.0f); break; // Mana (blue)
|
|
|
|
|
|
case 1: targetPowerColor = ImVec4(0.9f, 0.2f, 0.2f, 1.0f); break; // Rage (red)
|
2026-03-09 17:09:48 -07:00
|
|
|
|
case 2: targetPowerColor = ImVec4(0.9f, 0.6f, 0.1f, 1.0f); break; // Focus (orange)
|
2026-02-19 17:08:53 -08:00
|
|
|
|
case 3: targetPowerColor = ImVec4(0.9f, 0.9f, 0.2f, 1.0f); break; // Energy (yellow)
|
2026-03-09 17:09:48 -07:00
|
|
|
|
case 4: targetPowerColor = ImVec4(0.5f, 0.9f, 0.3f, 1.0f); break; // Happiness (green)
|
|
|
|
|
|
case 6: targetPowerColor = ImVec4(0.8f, 0.1f, 0.2f, 1.0f); break; // Runic Power (crimson)
|
|
|
|
|
|
case 7: targetPowerColor = ImVec4(0.4f, 0.1f, 0.6f, 1.0f); break; // Soul Shards (purple)
|
2026-02-19 17:08:53 -08:00
|
|
|
|
default: targetPowerColor = ImVec4(0.2f, 0.2f, 0.9f, 1.0f); break;
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, targetPowerColor);
|
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
|
|
|
|
char mpOverlay[64];
|
|
|
|
|
|
snprintf(mpOverlay, sizeof(mpOverlay), "%u / %u", targetPower, targetMaxPower);
|
2026-02-19 17:08:53 -08:00
|
|
|
|
ImGui::ProgressBar(mpPct, ImVec2(-1, 18), mpOverlay);
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextDisabled("No health data");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 23:06:40 -07: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();
|
2026-03-17 19:43:19 -07:00
|
|
|
|
bool interruptible = gameHandler.isTargetCastInterruptible();
|
2026-03-09 23:06:40 -07:00
|
|
|
|
const std::string& castName = (tspell != 0) ? gameHandler.getSpellName(tspell) : "";
|
2026-03-17 19:43:19 -07:00
|
|
|
|
// Color: interruptible = green (can Kick/CS), not interruptible = red, both pulse when >80%
|
2026-03-12 04:18:39 -07:00
|
|
|
|
ImVec4 castBarColor;
|
|
|
|
|
|
if (castPct > 0.8f) {
|
|
|
|
|
|
float pulse = 0.7f + 0.3f * std::sin(static_cast<float>(ImGui::GetTime()) * 8.0f);
|
2026-03-17 19:43:19 -07:00
|
|
|
|
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
|
2026-03-12 04:18:39 -07:00
|
|
|
|
} else {
|
2026-03-17 19:43:19 -07:00
|
|
|
|
castBarColor = interruptible ? ImVec4(0.2f, 0.75f, 0.2f, 1.0f) // green = can interrupt
|
|
|
|
|
|
: ImVec4(0.85f, 0.15f, 0.15f, 1.0f); // red = uninterruptible
|
2026-03-12 04:18:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, castBarColor);
|
2026-03-09 23:06:40 -07:00
|
|
|
|
char castLabel[72];
|
|
|
|
|
|
if (!castName.empty())
|
|
|
|
|
|
snprintf(castLabel, sizeof(castLabel), "%s (%.1fs)", castName.c_str(), castLeft);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(castLabel, sizeof(castLabel), "Casting... (%.1fs)", castLeft);
|
2026-03-12 08:03:43 -07:00
|
|
|
|
{
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-09 23:06:40 -07:00
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-13 09:06:48 -07: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);
|
|
|
|
|
|
}
|
|
|
|
|
|
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 = ImVec4(0.3f, 1.0f, 0.3f, 1.0f);
|
|
|
|
|
|
} else if (totEnt) {
|
|
|
|
|
|
totName = getEntityName(totEnt);
|
|
|
|
|
|
uint8_t cid = entityClassId(totEnt.get());
|
|
|
|
|
|
if (cid != 0) totColor = classColorVec4(cid);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!totName.empty()) {
|
|
|
|
|
|
ImGui::TextDisabled("▶");
|
|
|
|
|
|
ImGui::SameLine(0, 2);
|
|
|
|
|
|
ImGui::TextColored(totColor, "%s", totName.c_str());
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::SetTooltip("Target's target: %s\nClick to target", totName.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::IsItemClicked()) {
|
|
|
|
|
|
gameHandler.setTarget(totGuid);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08: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);
|
2026-02-23 06:37:15 -08:00
|
|
|
|
|
2026-03-12 02:59:09 -07: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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-23 06:37:15 -08: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;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
2026-03-12 06:08:26 -07: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-23 06:37:15 -08:00
|
|
|
|
int shown = 0;
|
2026-03-12 06:08:26 -07:00
|
|
|
|
for (size_t si = 0; si < sortedIdx.size() && shown < 16; ++si) {
|
|
|
|
|
|
size_t i = sortedIdx[si];
|
2026-02-23 06:37:15 -08:00
|
|
|
|
const auto& aura = targetAuras[i];
|
|
|
|
|
|
if (aura.isEmpty()) continue;
|
|
|
|
|
|
|
|
|
|
|
|
if (shown > 0 && shown % ICONS_PER_ROW != 0) ImGui::SameLine();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(10000 + i));
|
|
|
|
|
|
|
|
|
|
|
|
bool isBuff = (aura.flags & 0x80) == 0;
|
2026-03-12 08:20:14 -07:00
|
|
|
|
ImVec4 auraBorderColor;
|
|
|
|
|
|
if (isBuff) {
|
|
|
|
|
|
auraBorderColor = ImVec4(0.2f, 0.8f, 0.2f, 0.9f);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Debuff: color by dispel type, matching player buff bar convention
|
|
|
|
|
|
uint8_t dt = gameHandler.getSpellDispelType(aura.spellId);
|
|
|
|
|
|
switch (dt) {
|
|
|
|
|
|
case 1: auraBorderColor = ImVec4(0.15f, 0.50f, 1.00f, 0.9f); break; // magic: blue
|
|
|
|
|
|
case 2: auraBorderColor = ImVec4(0.70f, 0.20f, 0.90f, 0.9f); break; // curse: purple
|
|
|
|
|
|
case 3: auraBorderColor = ImVec4(0.55f, 0.30f, 0.10f, 0.9f); break; // disease: brown
|
|
|
|
|
|
case 4: auraBorderColor = ImVec4(0.10f, 0.70f, 0.10f, 0.9f); break; // poison: green
|
|
|
|
|
|
default: auraBorderColor = ImVec4(0.80f, 0.20f, 0.20f, 0.9f); break; // other: red
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-23 06:37:15 -08:00
|
|
|
|
|
|
|
|
|
|
VkDescriptorSet iconTex = VK_NULL_HANDLE;
|
|
|
|
|
|
if (assetMgr) {
|
|
|
|
|
|
iconTex = getSpellIcon(aura.spellId, assetMgr);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (iconTex) {
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, auraBorderColor);
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(1, 1));
|
|
|
|
|
|
ImGui::ImageButton("##taura",
|
|
|
|
|
|
(ImTextureID)(uintptr_t)iconTex,
|
|
|
|
|
|
ImVec2(ICON_SIZE - 2, ICON_SIZE - 2));
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, auraBorderColor);
|
|
|
|
|
|
char label[8];
|
|
|
|
|
|
snprintf(label, sizeof(label), "%u", aura.spellId);
|
|
|
|
|
|
ImGui::Button(label, ImVec2(ICON_SIZE, ICON_SIZE));
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 16:37:55 -07: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-03-17 19:04:40 -07: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-09 16:37:55 -07: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;
|
2026-03-12 04:44:48 -07:00
|
|
|
|
// Color by urgency (matches player buff bar)
|
|
|
|
|
|
ImU32 tTimerColor;
|
|
|
|
|
|
if (tRemainMs < 10000) {
|
|
|
|
|
|
float pulse = 0.7f + 0.3f * std::sin(
|
|
|
|
|
|
static_cast<float>(ImGui::GetTime()) * 6.0f);
|
|
|
|
|
|
tTimerColor = IM_COL32(
|
|
|
|
|
|
static_cast<int>(255 * pulse),
|
|
|
|
|
|
static_cast<int>(80 * pulse),
|
|
|
|
|
|
static_cast<int>(60 * pulse), 255);
|
|
|
|
|
|
} else if (tRemainMs < 30000) {
|
|
|
|
|
|
tTimerColor = IM_COL32(255, 165, 0, 255);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
tTimerColor = IM_COL32(255, 255, 255, 255);
|
|
|
|
|
|
}
|
2026-03-09 16:37:55 -07:00
|
|
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(cx + 1, cy + 1),
|
|
|
|
|
|
IM_COL32(0, 0, 0, 200), timeStr);
|
|
|
|
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(cx, cy),
|
2026-03-12 04:44:48 -07:00
|
|
|
|
tTimerColor, timeStr);
|
2026-03-09 16:37:55 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 17:08:14 -07: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-10 19:33:25 -07:00
|
|
|
|
// Tooltip: rich spell info + remaining duration
|
2026-02-23 06:37:15 -08:00
|
|
|
|
if (ImGui::IsItemHovered()) {
|
2026-03-10 19:33:25 -07:00
|
|
|
|
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-03-09 16:37:55 -07:00
|
|
|
|
if (tRemainMs > 0) {
|
|
|
|
|
|
int seconds = tRemainMs / 1000;
|
2026-03-10 19:33:25 -07:00
|
|
|
|
char durBuf[32];
|
|
|
|
|
|
if (seconds < 60) snprintf(durBuf, sizeof(durBuf), "Remaining: %ds", seconds);
|
|
|
|
|
|
else snprintf(durBuf, sizeof(durBuf), "Remaining: %dm %ds", seconds / 60, seconds % 60);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", durBuf);
|
2026-02-23 06:37:15 -08:00
|
|
|
|
}
|
2026-03-10 19:33:25 -07:00
|
|
|
|
ImGui::EndTooltip();
|
2026-02-23 06:37:15 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
shown++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
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();
|
2026-03-09 16:49:50 -07: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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (totGuid != 0) {
|
|
|
|
|
|
auto totEntity = gameHandler.getEntityManager().getEntity(totGuid);
|
|
|
|
|
|
if (totEntity) {
|
|
|
|
|
|
// Position ToT frame just below and right-aligned with the target frame
|
|
|
|
|
|
float totW = 160.0f;
|
|
|
|
|
|
float totX = (screenW - totW) / 2.0f + (frameW - totW);
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(totX, 30.0f + 130.0f), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(totW, 0.0f), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.08f, 0.80f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.4f, 0.4f, 0.4f, 0.7f));
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##ToTFrame", nullptr,
|
|
|
|
|
|
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
|
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar)) {
|
|
|
|
|
|
std::string totName = getEntityName(totEntity);
|
2026-03-12 08:50:14 -07:00
|
|
|
|
// Class color for players; gray for NPCs
|
|
|
|
|
|
ImVec4 totNameColor = ImVec4(0.8f, 0.8f, 0.8f, 1.0f);
|
|
|
|
|
|
if (totEntity->getType() == game::ObjectType::PLAYER) {
|
|
|
|
|
|
uint8_t cid = entityClassId(totEntity.get());
|
|
|
|
|
|
if (cid != 0) totNameColor = classColorVec4(cid);
|
|
|
|
|
|
}
|
2026-03-11 23:50:41 -07:00
|
|
|
|
// Selectable so we can attach a right-click context menu
|
2026-03-12 08:50:14 -07:00
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, totNameColor);
|
2026-03-11 23:50:41 -07:00
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0,0,0,0));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(1,1,1,0.08f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(1,1,1,0.12f));
|
|
|
|
|
|
if (ImGui::Selectable(totName.c_str(), false,
|
|
|
|
|
|
ImGuiSelectableFlags_DontClosePopups,
|
|
|
|
|
|
ImVec2(ImGui::CalcTextSize(totName.c_str()).x, 0))) {
|
|
|
|
|
|
gameHandler.setTarget(totGuid);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor(4);
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::BeginPopupContextItem("##ToTCtx")) {
|
|
|
|
|
|
ImGui::TextDisabled("%s", totName.c_str());
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Target"))
|
|
|
|
|
|
gameHandler.setTarget(totGuid);
|
|
|
|
|
|
if (ImGui::MenuItem("Set Focus"))
|
|
|
|
|
|
gameHandler.setFocus(totGuid);
|
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
2026-03-09 16:49:50 -07:00
|
|
|
|
|
|
|
|
|
|
if (totEntity->getType() == game::ObjectType::UNIT ||
|
|
|
|
|
|
totEntity->getType() == game::ObjectType::PLAYER) {
|
|
|
|
|
|
auto totUnit = std::static_pointer_cast<game::Unit>(totEntity);
|
2026-03-10 21:19:42 -07:00
|
|
|
|
if (totUnit->getLevel() > 0) {
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextDisabled("Lv%u", totUnit->getLevel());
|
|
|
|
|
|
}
|
2026-03-09 16:49:50 -07: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 ? ImVec4(0.2f, 0.7f, 0.2f, 1.0f) :
|
|
|
|
|
|
pct > 0.2f ? ImVec4(0.7f, 0.7f, 0.2f, 1.0f) :
|
|
|
|
|
|
ImVec4(0.7f, 0.2f, 0.2f, 1.0f));
|
|
|
|
|
|
ImGui::ProgressBar(pct, ImVec2(-1, 10), "");
|
|
|
|
|
|
ImGui::PopStyleColor();
|
2026-03-12 13:58:30 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 20:02:02 -07:00
|
|
|
|
// ToT cast bar — green if interruptible, red if not; pulses near completion
|
2026-03-12 13:58:30 -07:00
|
|
|
|
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);
|
2026-03-17 20:02:02 -07:00
|
|
|
|
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);
|
2026-03-12 13:58:30 -07:00
|
|
|
|
} else {
|
2026-03-17 20:02:02 -07:00
|
|
|
|
tcColor = totCs->interruptible
|
|
|
|
|
|
? ImVec4(0.2f, 0.75f, 0.2f, 1.0f)
|
|
|
|
|
|
: ImVec4(0.85f, 0.15f, 0.15f, 1.0f);
|
2026-03-12 13:58:30 -07:00
|
|
|
|
}
|
|
|
|
|
|
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();
|
2026-03-09 16:49:50 -07:00
|
|
|
|
}
|
2026-03-12 13:39:36 -07:00
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
VkDescriptorSet taIcon = (totAsset)
|
|
|
|
|
|
? getSpellIcon(aura.spellId, totAsset) : VK_NULL_HANDLE;
|
|
|
|
|
|
if (taIcon) {
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, borderCol);
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(1, 1));
|
|
|
|
|
|
ImGui::ImageButton("##taura",
|
|
|
|
|
|
(ImTextureID)(uintptr_t)taIcon,
|
|
|
|
|
|
ImVec2(TA_ICON - 2, TA_ICON - 2));
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, borderCol);
|
|
|
|
|
|
char lab[8];
|
|
|
|
|
|
snprintf(lab, sizeof(lab), "%u", aura.spellId % 10000);
|
|
|
|
|
|
ImGui::Button(lab, ImVec2(TA_ICON, TA_ICON));
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Duration overlay
|
|
|
|
|
|
int32_t taRemain = aura.getRemainingMs(taNowMs);
|
|
|
|
|
|
if (taRemain > 0) {
|
|
|
|
|
|
ImVec2 imin = ImGui::GetItemRectMin();
|
|
|
|
|
|
ImVec2 imax = ImGui::GetItemRectMax();
|
|
|
|
|
|
char ts[12];
|
|
|
|
|
|
int s = (taRemain + 999) / 1000;
|
|
|
|
|
|
if (s >= 3600) snprintf(ts, sizeof(ts), "%dh", s / 3600);
|
|
|
|
|
|
else if (s >= 60) snprintf(ts, sizeof(ts), "%d:%02d", s / 60, s % 60);
|
|
|
|
|
|
else snprintf(ts, sizeof(ts), "%d", s);
|
|
|
|
|
|
ImVec2 tsz = ImGui::CalcTextSize(ts);
|
|
|
|
|
|
float cx = imin.x + (imax.x - imin.x - tsz.x) * 0.5f;
|
|
|
|
|
|
float cy = imax.y - tsz.y;
|
|
|
|
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(cx + 1, cy + 1), IM_COL32(0, 0, 0, 180), ts);
|
|
|
|
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(cx, cy), IM_COL32(255, 255, 255, 220), ts);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Tooltip
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
bool richOk = spellbookScreen.renderSpellInfoTooltip(
|
|
|
|
|
|
aura.spellId, gameHandler, totAsset);
|
|
|
|
|
|
if (!richOk) {
|
|
|
|
|
|
std::string nm = spellbookScreen.lookupSpellName(aura.spellId, totAsset);
|
|
|
|
|
|
if (nm.empty()) nm = "Spell #" + std::to_string(aura.spellId);
|
|
|
|
|
|
ImGui::Text("%s", nm.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
if (taRemain > 0) {
|
|
|
|
|
|
int s = taRemain / 1000;
|
|
|
|
|
|
char db[32];
|
|
|
|
|
|
if (s < 60) snprintf(db, sizeof(db), "Remaining: %ds", s);
|
|
|
|
|
|
else snprintf(db, sizeof(db), "Remaining: %dm %ds", s / 60, s % 60);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", db);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
taShown++;
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-09 16:49:50 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 21:15:24 -07:00
|
|
|
|
void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) {
|
|
|
|
|
|
auto focus = gameHandler.getFocus();
|
|
|
|
|
|
if (!focus) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
|
|
|
|
|
|
// Position: right side of screen, mirroring the target frame on the opposite side
|
|
|
|
|
|
float frameW = 200.0f;
|
|
|
|
|
|
float frameX = screenW - frameW - 10.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(frameX, 30.0f), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(frameW, 0.0f), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize;
|
|
|
|
|
|
|
|
|
|
|
|
// Determine color based on relation (same logic as target frame)
|
|
|
|
|
|
ImVec4 focusColor(0.7f, 0.7f, 0.7f, 1.0f);
|
|
|
|
|
|
if (focus->getType() == game::ObjectType::PLAYER) {
|
2026-03-12 08:37:14 -07:00
|
|
|
|
// Use class color for player focus targets
|
|
|
|
|
|
uint8_t cid = entityClassId(focus.get());
|
|
|
|
|
|
focusColor = (cid != 0) ? classColorVec4(cid) : ImVec4(0.3f, 1.0f, 0.3f, 1.0f);
|
2026-03-10 21:15:24 -07:00
|
|
|
|
} else if (focus->getType() == game::ObjectType::UNIT) {
|
|
|
|
|
|
auto u = std::static_pointer_cast<game::Unit>(focus);
|
|
|
|
|
|
if (u->getHealth() == 0 && u->getMaxHealth() > 0) {
|
|
|
|
|
|
focusColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
|
|
|
|
|
|
} else if (u->isHostile()) {
|
|
|
|
|
|
uint32_t playerLv = gameHandler.getPlayerLevel();
|
|
|
|
|
|
uint32_t mobLv = u->getLevel();
|
2026-03-17 14:18:49 -07:00
|
|
|
|
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 = ImVec4(0.6f, 0.6f, 0.6f, 1.0f);
|
|
|
|
|
|
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 = ImVec4(0.3f, 1.0f, 0.3f, 1.0f);
|
|
|
|
|
|
}
|
2026-03-10 21:15:24 -07:00
|
|
|
|
} else {
|
|
|
|
|
|
focusColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.15f, 0.85f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.5f, 0.5f, 0.9f, 0.8f)); // Blue tint = focus
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##FocusFrame", nullptr, flags)) {
|
|
|
|
|
|
// "Focus" label
|
|
|
|
|
|
ImGui::TextDisabled("[Focus]");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
|
2026-03-12 14:30:15 -07: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-10 21:15:24 -07:00
|
|
|
|
std::string focusName = getEntityName(focus);
|
2026-03-11 23:51:27 -07:00
|
|
|
|
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 14:43:58 -07: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(ImVec4(1.0f, 0.85f, 0.1f, 1.0f), "\xe2\x99\x9b");
|
|
|
|
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Group Leader");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:21:02 -07: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);
|
|
|
|
|
|
|
|
|
|
|
|
// Quest indicator: ! / ?
|
|
|
|
|
|
{
|
|
|
|
|
|
using QGS = game::QuestGiverStatus;
|
|
|
|
|
|
QGS qgs = gameHandler.getQuestGiverStatus(focus->getGuid());
|
|
|
|
|
|
if (qgs == QGS::AVAILABLE) {
|
|
|
|
|
|
ImGui::SameLine(0, 4);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "!");
|
|
|
|
|
|
} else if (qgs == QGS::AVAILABLE_LOW) {
|
|
|
|
|
|
ImGui::SameLine(0, 4);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "!");
|
|
|
|
|
|
} else if (qgs == QGS::REWARD || qgs == QGS::REWARD_REP) {
|
|
|
|
|
|
ImGui::SameLine(0, 4);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "?");
|
|
|
|
|
|
} else if (qgs == QGS::INCOMPLETE) {
|
|
|
|
|
|
ImGui::SameLine(0, 4);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "?");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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(ImVec4(1.0f,0.3f,0.3f,1.0f), "[Boss]"); }
|
|
|
|
|
|
else if (fRank == 4) { ImGui::SameLine(0,4); ImGui::TextColored(ImVec4(0.5f,0.9f,1.0f,1.0f), "[Rare]"); }
|
|
|
|
|
|
|
|
|
|
|
|
// 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-11 23:51:27 -07: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"))
|
|
|
|
|
|
gameHandler.clearFocus();
|
|
|
|
|
|
if (focusIsPlayer) {
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Whisper")) {
|
|
|
|
|
|
selectedChatType = 4;
|
|
|
|
|
|
strncpy(whisperTargetBuffer, focusName.c_str(), sizeof(whisperTargetBuffer) - 1);
|
|
|
|
|
|
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Invite to Group"))
|
|
|
|
|
|
gameHandler.inviteToGroup(focusName);
|
|
|
|
|
|
if (ImGui::MenuItem("Trade"))
|
|
|
|
|
|
gameHandler.initiateTrade(fGuid);
|
2026-03-11 23:56:57 -07:00
|
|
|
|
if (ImGui::MenuItem("Duel"))
|
|
|
|
|
|
gameHandler.proposeDuel(fGuid);
|
|
|
|
|
|
if (ImGui::MenuItem("Inspect")) {
|
|
|
|
|
|
gameHandler.setTarget(fGuid);
|
|
|
|
|
|
gameHandler.inspectTarget();
|
2026-03-12 02:52:40 -07:00
|
|
|
|
showInspectWindow_ = true;
|
2026-03-11 23:56:57 -07:00
|
|
|
|
}
|
2026-03-12 00:26:47 -07:00
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Add Friend"))
|
|
|
|
|
|
gameHandler.addFriend(focusName);
|
|
|
|
|
|
if (ImGui::MenuItem("Ignore"))
|
|
|
|
|
|
gameHandler.addIgnore(focusName);
|
2026-03-11 23:51:27 -07:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
2026-03-10 21:15:24 -07:00
|
|
|
|
|
|
|
|
|
|
if (focus->getType() == game::ObjectType::UNIT ||
|
|
|
|
|
|
focus->getType() == game::ObjectType::PLAYER) {
|
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(focus);
|
|
|
|
|
|
|
|
|
|
|
|
// Level + health on same row
|
|
|
|
|
|
ImGui::SameLine();
|
2026-03-17 14:18:49 -07:00
|
|
|
|
if (unit->getLevel() == 0)
|
|
|
|
|
|
ImGui::TextDisabled("Lv ??");
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::TextDisabled("Lv %u", unit->getLevel());
|
2026-03-10 21:15:24 -07: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 ? ImVec4(0.2f, 0.7f, 0.2f, 1.0f) :
|
|
|
|
|
|
pct > 0.2f ? ImVec4(0.7f, 0.7f, 0.2f, 1.0f) :
|
|
|
|
|
|
ImVec4(0.7f, 0.2f, 0.2f, 1.0f));
|
|
|
|
|
|
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 = ImVec4(0.2f, 0.2f, 0.9f, 1.0f); break;
|
|
|
|
|
|
case 1: pwrColor = ImVec4(0.9f, 0.2f, 0.2f, 1.0f); break;
|
|
|
|
|
|
case 3: pwrColor = ImVec4(0.9f, 0.9f, 0.2f, 1.0f); break;
|
|
|
|
|
|
case 6: pwrColor = ImVec4(0.8f, 0.1f, 0.2f, 1.0f); break;
|
|
|
|
|
|
default: pwrColor = ImVec4(0.2f, 0.2f, 0.9f, 1.0f); break;
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, pwrColor);
|
|
|
|
|
|
ImGui::ProgressBar(mpPct, ImVec2(-1, 10), "");
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Focus cast bar
|
|
|
|
|
|
const auto* focusCast = gameHandler.getUnitCastState(focus->getGuid());
|
|
|
|
|
|
if (focusCast) {
|
|
|
|
|
|
float total = focusCast->timeTotal > 0.f ? focusCast->timeTotal : 1.f;
|
|
|
|
|
|
float rem = focusCast->timeRemaining;
|
|
|
|
|
|
float prog = std::clamp(1.0f - rem / total, 0.f, 1.f);
|
|
|
|
|
|
const std::string& spName = gameHandler.getSpellName(focusCast->spellId);
|
2026-03-12 04:18:39 -07:00
|
|
|
|
// Pulse orange when > 80% complete — interrupt window closing
|
|
|
|
|
|
ImVec4 focusCastColor;
|
|
|
|
|
|
if (prog > 0.8f) {
|
|
|
|
|
|
float pulse = 0.7f + 0.3f * std::sin(static_cast<float>(ImGui::GetTime()) * 8.0f);
|
|
|
|
|
|
focusCastColor = ImVec4(1.0f * pulse, 0.5f * pulse, 0.0f, 1.0f);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
focusCastColor = ImVec4(0.9f, 0.3f, 0.2f, 1.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, focusCastColor);
|
2026-03-10 21:15:24 -07:00
|
|
|
|
char castBuf[64];
|
|
|
|
|
|
if (!spName.empty())
|
|
|
|
|
|
snprintf(castBuf, sizeof(castBuf), "%s (%.1fs)", spName.c_str(), rem);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(castBuf, sizeof(castBuf), "Casting... (%.1fs)", rem);
|
2026-03-12 08:03:43 -07:00
|
|
|
|
{
|
|
|
|
|
|
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);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::ProgressBar(prog, ImVec2(-1, 12), castBuf);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-10 21:15:24 -07:00
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
2026-03-12 13:32:10 -07: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());
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Duration overlay
|
|
|
|
|
|
int32_t faRemain = aura.getRemainingMs(faNowMs);
|
|
|
|
|
|
if (faRemain > 0) {
|
|
|
|
|
|
ImVec2 imin = ImGui::GetItemRectMin();
|
|
|
|
|
|
ImVec2 imax = ImGui::GetItemRectMax();
|
|
|
|
|
|
char ts[12];
|
|
|
|
|
|
int s = (faRemain + 999) / 1000;
|
|
|
|
|
|
if (s >= 3600) snprintf(ts, sizeof(ts), "%dh", s / 3600);
|
|
|
|
|
|
else if (s >= 60) snprintf(ts, sizeof(ts), "%d:%02d", s / 60, s % 60);
|
|
|
|
|
|
else snprintf(ts, sizeof(ts), "%d", s);
|
|
|
|
|
|
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-03-12 14:50:59 -07: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-03-12 13:32:10 -07: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());
|
|
|
|
|
|
}
|
|
|
|
|
|
if (faRemain > 0) {
|
|
|
|
|
|
int s = faRemain / 1000;
|
|
|
|
|
|
char db[32];
|
|
|
|
|
|
if (s < 60) snprintf(db, sizeof(db), "Remaining: %ds", s);
|
|
|
|
|
|
else snprintf(db, sizeof(db), "Remaining: %dm %ds", s / 60, s % 60);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", db);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
faShown++;
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-10 21:15:24 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:34:21 -07: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-03-10 21:15:24 -07:00
|
|
|
|
// Clicking the focus frame targets it
|
|
|
|
|
|
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(0)) {
|
|
|
|
|
|
gameHandler.setTarget(focus->getGuid());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (strlen(chatInputBuffer) > 0) {
|
|
|
|
|
|
std::string input(chatInputBuffer);
|
2026-03-11 23:06:24 -07:00
|
|
|
|
|
|
|
|
|
|
// Save to sent-message history (skip pure whitespace, cap at 50 entries)
|
|
|
|
|
|
{
|
|
|
|
|
|
bool allSpace = true;
|
|
|
|
|
|
for (char c : input) { if (!std::isspace(static_cast<unsigned char>(c))) { allSpace = false; break; } }
|
|
|
|
|
|
if (!allSpace) {
|
|
|
|
|
|
// Remove duplicate of last entry if identical
|
|
|
|
|
|
if (chatSentHistory_.empty() || chatSentHistory_.back() != input) {
|
|
|
|
|
|
chatSentHistory_.push_back(input);
|
|
|
|
|
|
if (chatSentHistory_.size() > 50)
|
|
|
|
|
|
chatSentHistory_.erase(chatSentHistory_.begin());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
chatHistoryIdx_ = -1; // reset browsing position after send
|
|
|
|
|
|
|
2026-03-09 15:52:58 -07:00
|
|
|
|
game::ChatType type = game::ChatType::SAY;
|
2026-02-07 12:30:36 -08:00
|
|
|
|
std::string message = input;
|
|
|
|
|
|
std::string target;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-07 23:32:27 -08:00
|
|
|
|
// Track if a channel shortcut should change the chat type dropdown
|
|
|
|
|
|
int switchChatType = -1;
|
|
|
|
|
|
|
2026-02-07 12:30:36 -08:00
|
|
|
|
// Check for slash commands
|
2026-02-02 12:24:50 -08:00
|
|
|
|
if (input.size() > 1 && input[0] == '/') {
|
|
|
|
|
|
std::string command = input.substr(1);
|
2026-02-07 12:30:36 -08:00
|
|
|
|
size_t spacePos = command.find(' ');
|
|
|
|
|
|
std::string cmd = (spacePos != std::string::npos) ? command.substr(0, spacePos) : command;
|
|
|
|
|
|
|
|
|
|
|
|
// Convert command to lowercase for comparison
|
|
|
|
|
|
std::string cmdLower = cmd;
|
|
|
|
|
|
for (char& c : cmdLower) c = std::tolower(c);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-07 12:30:36 -08:00
|
|
|
|
// Special commands
|
|
|
|
|
|
if (cmdLower == "logout") {
|
2026-02-05 15:59:06 -08:00
|
|
|
|
core::Application::getInstance().logoutToLogin();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-13 09:38:39 -07:00
|
|
|
|
if (cmdLower == "clear") {
|
|
|
|
|
|
gameHandler.clearChatHistory();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 12:30:36 -08:00
|
|
|
|
// /invite command
|
|
|
|
|
|
if (cmdLower == "invite" && spacePos != std::string::npos) {
|
|
|
|
|
|
std::string targetName = command.substr(spacePos + 1);
|
|
|
|
|
|
gameHandler.inviteToGroup(targetName);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-07 12:37:13 -08:00
|
|
|
|
// /inspect command
|
|
|
|
|
|
if (cmdLower == "inspect") {
|
|
|
|
|
|
gameHandler.inspectTarget();
|
2026-03-12 02:52:40 -07:00
|
|
|
|
showInspectWindow_ = true;
|
2026-02-07 12:37:13 -08:00
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 02:59:09 -07:00
|
|
|
|
// /threat command
|
|
|
|
|
|
if (cmdLower == "threat") {
|
|
|
|
|
|
showThreatWindow_ = !showThreatWindow_;
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 12:02:59 -07:00
|
|
|
|
// /score command — BG scoreboard
|
|
|
|
|
|
if (cmdLower == "score") {
|
|
|
|
|
|
gameHandler.requestPvpLog();
|
|
|
|
|
|
showBgScoreboard_ = true;
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 12:43:32 -08:00
|
|
|
|
// /time command
|
|
|
|
|
|
if (cmdLower == "time") {
|
|
|
|
|
|
gameHandler.queryServerTime();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 09:36:14 -07:00
|
|
|
|
// /zone command — print current zone name
|
|
|
|
|
|
if (cmdLower == "zone") {
|
|
|
|
|
|
std::string zoneName;
|
|
|
|
|
|
if (auto* rend = core::Application::getInstance().getRenderer())
|
|
|
|
|
|
zoneName = rend->getCurrentZoneName();
|
|
|
|
|
|
game::MessageChatData sysMsg;
|
|
|
|
|
|
sysMsg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
sysMsg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
sysMsg.message = zoneName.empty() ? "You are not in a known zone." : "You are in: " + zoneName;
|
|
|
|
|
|
gameHandler.addLocalChatMessage(sysMsg);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 12:43:32 -08:00
|
|
|
|
// /played command
|
|
|
|
|
|
if (cmdLower == "played") {
|
|
|
|
|
|
gameHandler.requestPlayedTime();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 02:31:12 -07:00
|
|
|
|
// /ticket command — open GM ticket window
|
|
|
|
|
|
if (cmdLower == "ticket" || cmdLower == "gmticket" || cmdLower == "gm") {
|
|
|
|
|
|
showGmTicketWindow_ = true;
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
2026-03-12 09:45:03 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /chathelp command — list chat-channel slash commands
|
|
|
|
|
|
if (cmdLower == "chathelp") {
|
|
|
|
|
|
static const char* kChatHelp[] = {
|
|
|
|
|
|
"--- Chat Channel Commands ---",
|
|
|
|
|
|
"/s [msg] Say to nearby players",
|
|
|
|
|
|
"/y [msg] Yell to a wider area",
|
|
|
|
|
|
"/w <name> [msg] Whisper to player",
|
|
|
|
|
|
"/r [msg] Reply to last whisper",
|
|
|
|
|
|
"/p [msg] Party chat",
|
|
|
|
|
|
"/g [msg] Guild chat",
|
|
|
|
|
|
"/o [msg] Guild officer chat",
|
|
|
|
|
|
"/raid [msg] Raid chat",
|
|
|
|
|
|
"/rw [msg] Raid warning",
|
|
|
|
|
|
"/bg [msg] Battleground chat",
|
|
|
|
|
|
"/1 [msg] General channel",
|
|
|
|
|
|
"/2 [msg] Trade channel (also /wts /wtb)",
|
|
|
|
|
|
"/<N> [msg] Channel by number",
|
|
|
|
|
|
"/join <chan> Join a channel",
|
|
|
|
|
|
"/leave <chan> Leave a channel",
|
|
|
|
|
|
"/afk [msg] Set AFK status",
|
|
|
|
|
|
"/dnd [msg] Set Do Not Disturb",
|
|
|
|
|
|
};
|
|
|
|
|
|
for (const char* line : kChatHelp) {
|
|
|
|
|
|
game::MessageChatData helpMsg;
|
|
|
|
|
|
helpMsg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
helpMsg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
helpMsg.message = line;
|
|
|
|
|
|
gameHandler.addLocalChatMessage(helpMsg);
|
|
|
|
|
|
}
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
2026-03-12 02:31:12 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 02:23:24 -07:00
|
|
|
|
// /help command — list available slash commands
|
|
|
|
|
|
if (cmdLower == "help" || cmdLower == "?") {
|
|
|
|
|
|
static const char* kHelpLines[] = {
|
|
|
|
|
|
"--- Wowee Slash Commands ---",
|
|
|
|
|
|
"Chat: /s /y /p /g /raid /rw /o /bg /w <name> [msg] /r [msg]",
|
|
|
|
|
|
"Social: /who [filter] /whois <name> /friend add/remove <name>",
|
|
|
|
|
|
" /ignore <name> /unignore <name>",
|
|
|
|
|
|
"Party: /invite <name> /uninvite <name> /leave /readycheck",
|
|
|
|
|
|
" /maintank /mainassist /roll [min-max]",
|
|
|
|
|
|
"Guild: /ginvite /gkick /gquit /gpromote /gdemote /gmotd",
|
|
|
|
|
|
" /gleader /groster /ginfo /gcreate /gdisband",
|
2026-03-12 09:36:14 -07:00
|
|
|
|
"Combat: /startattack /stopattack /stopcasting /cast <spell> /duel /pvp",
|
|
|
|
|
|
" /forfeit /follow /stopfollow /assist",
|
2026-03-12 10:06:11 -07:00
|
|
|
|
"Items: /use <item name> /equip <item name>",
|
2026-03-12 02:23:24 -07:00
|
|
|
|
"Target: /target <name> /cleartarget /focus /clearfocus",
|
|
|
|
|
|
"Movement: /sit /stand /kneel /dismount",
|
2026-03-12 09:36:14 -07:00
|
|
|
|
"Misc: /played /time /zone /afk [msg] /dnd [msg] /inspect",
|
2026-03-12 02:23:24 -07:00
|
|
|
|
" /helm /cloak /trade /join <channel> /leave <channel>",
|
2026-03-12 12:02:59 -07:00
|
|
|
|
" /score /unstuck /logout /ticket /help",
|
2026-03-12 02:23:24 -07:00
|
|
|
|
};
|
|
|
|
|
|
for (const char* line : kHelpLines) {
|
|
|
|
|
|
game::MessageChatData helpMsg;
|
|
|
|
|
|
helpMsg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
helpMsg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
helpMsg.message = line;
|
|
|
|
|
|
gameHandler.addLocalChatMessage(helpMsg);
|
|
|
|
|
|
}
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-11 21:14:35 -08:00
|
|
|
|
// /who commands
|
|
|
|
|
|
if (cmdLower == "who" || cmdLower == "whois" || cmdLower == "online" || cmdLower == "players") {
|
|
|
|
|
|
std::string query;
|
2026-02-07 12:43:32 -08:00
|
|
|
|
if (spacePos != std::string::npos) {
|
2026-02-11 21:14:35 -08:00
|
|
|
|
query = command.substr(spacePos + 1);
|
|
|
|
|
|
// Trim leading/trailing whitespace
|
|
|
|
|
|
size_t first = query.find_first_not_of(" \t\r\n");
|
|
|
|
|
|
if (first == std::string::npos) {
|
|
|
|
|
|
query.clear();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
size_t last = query.find_last_not_of(" \t\r\n");
|
|
|
|
|
|
query = query.substr(first, last - first + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ((cmdLower == "whois") && query.empty()) {
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "Usage: /whois <playerName>";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "who" && (query == "help" || query == "?")) {
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "Who commands: /who [name/filter], /whois <name>, /online";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
2026-02-07 12:43:32 -08:00
|
|
|
|
}
|
2026-02-11 21:14:35 -08:00
|
|
|
|
|
|
|
|
|
|
gameHandler.queryWho(query);
|
2026-03-12 10:41:18 -07:00
|
|
|
|
showWhoWindow_ = true;
|
2026-02-07 12:43:32 -08:00
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
// /combatlog command
|
|
|
|
|
|
if (cmdLower == "combatlog" || cmdLower == "cl") {
|
|
|
|
|
|
showCombatLog_ = !showCombatLog_;
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
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
|
|
|
|
// /roll command
|
|
|
|
|
|
if (cmdLower == "roll" || cmdLower == "random" || cmdLower == "rnd") {
|
|
|
|
|
|
uint32_t minRoll = 1;
|
|
|
|
|
|
uint32_t maxRoll = 100;
|
|
|
|
|
|
|
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
|
std::string args = command.substr(spacePos + 1);
|
|
|
|
|
|
size_t dashPos = args.find('-');
|
|
|
|
|
|
size_t spacePos2 = args.find(' ');
|
|
|
|
|
|
|
|
|
|
|
|
if (dashPos != std::string::npos) {
|
|
|
|
|
|
// Format: /roll 1-100
|
|
|
|
|
|
try {
|
|
|
|
|
|
minRoll = std::stoul(args.substr(0, dashPos));
|
|
|
|
|
|
maxRoll = std::stoul(args.substr(dashPos + 1));
|
|
|
|
|
|
} catch (...) {}
|
|
|
|
|
|
} else if (spacePos2 != std::string::npos) {
|
|
|
|
|
|
// Format: /roll 1 100
|
|
|
|
|
|
try {
|
|
|
|
|
|
minRoll = std::stoul(args.substr(0, spacePos2));
|
|
|
|
|
|
maxRoll = std::stoul(args.substr(spacePos2 + 1));
|
|
|
|
|
|
} catch (...) {}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Format: /roll 100 (means 1-100)
|
|
|
|
|
|
try {
|
|
|
|
|
|
maxRoll = std::stoul(args);
|
|
|
|
|
|
} catch (...) {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
gameHandler.randomRoll(minRoll, maxRoll);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /friend or /addfriend command
|
|
|
|
|
|
if (cmdLower == "friend" || cmdLower == "addfriend") {
|
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
|
std::string args = command.substr(spacePos + 1);
|
|
|
|
|
|
size_t subCmdSpace = args.find(' ');
|
|
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "friend" && subCmdSpace != std::string::npos) {
|
|
|
|
|
|
std::string subCmd = args.substr(0, subCmdSpace);
|
|
|
|
|
|
std::transform(subCmd.begin(), subCmd.end(), subCmd.begin(), ::tolower);
|
|
|
|
|
|
|
|
|
|
|
|
if (subCmd == "add") {
|
|
|
|
|
|
std::string playerName = args.substr(subCmdSpace + 1);
|
|
|
|
|
|
gameHandler.addFriend(playerName);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
} else if (subCmd == "remove" || subCmd == "delete" || subCmd == "rem") {
|
|
|
|
|
|
std::string playerName = args.substr(subCmdSpace + 1);
|
|
|
|
|
|
gameHandler.removeFriend(playerName);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// /addfriend name or /friend name (assume add)
|
|
|
|
|
|
gameHandler.addFriend(args);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "Usage: /friend add <name> or /friend remove <name>";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /removefriend or /delfriend command
|
|
|
|
|
|
if (cmdLower == "removefriend" || cmdLower == "delfriend" || cmdLower == "remfriend") {
|
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
|
std::string playerName = command.substr(spacePos + 1);
|
|
|
|
|
|
gameHandler.removeFriend(playerName);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "Usage: /removefriend <name>";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 12:58:11 -08:00
|
|
|
|
// /ignore command
|
|
|
|
|
|
if (cmdLower == "ignore") {
|
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
|
std::string playerName = command.substr(spacePos + 1);
|
|
|
|
|
|
gameHandler.addIgnore(playerName);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "Usage: /ignore <name>";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /unignore command
|
|
|
|
|
|
if (cmdLower == "unignore") {
|
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
|
std::string playerName = command.substr(spacePos + 1);
|
|
|
|
|
|
gameHandler.removeIgnore(playerName);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "Usage: /unignore <name>";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 17:59:40 -08:00
|
|
|
|
// /dismount command
|
|
|
|
|
|
if (cmdLower == "dismount") {
|
|
|
|
|
|
gameHandler.dismount();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 12:58:11 -08:00
|
|
|
|
// /sit command
|
|
|
|
|
|
if (cmdLower == "sit") {
|
|
|
|
|
|
gameHandler.setStandState(1); // 1 = sit
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /stand command
|
|
|
|
|
|
if (cmdLower == "stand") {
|
|
|
|
|
|
gameHandler.setStandState(0); // 0 = stand
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /kneel command
|
|
|
|
|
|
if (cmdLower == "kneel") {
|
|
|
|
|
|
gameHandler.setStandState(8); // 8 = kneel
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /logout command (already exists but using /logout instead of going to login)
|
|
|
|
|
|
if (cmdLower == "logout" || cmdLower == "camp") {
|
|
|
|
|
|
gameHandler.requestLogout();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /cancellogout command
|
|
|
|
|
|
if (cmdLower == "cancellogout") {
|
|
|
|
|
|
gameHandler.cancelLogout();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 13:03:21 -08:00
|
|
|
|
// /helm command
|
|
|
|
|
|
if (cmdLower == "helm" || cmdLower == "helmet" || cmdLower == "showhelm") {
|
|
|
|
|
|
gameHandler.toggleHelm();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /cloak command
|
|
|
|
|
|
if (cmdLower == "cloak" || cmdLower == "showcloak") {
|
|
|
|
|
|
gameHandler.toggleCloak();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /follow command
|
|
|
|
|
|
if (cmdLower == "follow" || cmdLower == "f") {
|
|
|
|
|
|
gameHandler.followTarget();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 09:36:14 -07:00
|
|
|
|
// /stopfollow command
|
|
|
|
|
|
if (cmdLower == "stopfollow") {
|
|
|
|
|
|
gameHandler.cancelFollow();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 13:03:21 -08:00
|
|
|
|
// /assist command
|
|
|
|
|
|
if (cmdLower == "assist") {
|
|
|
|
|
|
gameHandler.assistTarget();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
// /pvp command
|
|
|
|
|
|
if (cmdLower == "pvp") {
|
|
|
|
|
|
gameHandler.togglePvp();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /ginfo command
|
|
|
|
|
|
if (cmdLower == "ginfo" || cmdLower == "guildinfo") {
|
|
|
|
|
|
gameHandler.requestGuildInfo();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /groster command
|
|
|
|
|
|
if (cmdLower == "groster" || cmdLower == "guildroster") {
|
|
|
|
|
|
gameHandler.requestGuildRoster();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /gmotd command
|
|
|
|
|
|
if (cmdLower == "gmotd" || cmdLower == "guildmotd") {
|
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
|
std::string motd = command.substr(spacePos + 1);
|
|
|
|
|
|
gameHandler.setGuildMotd(motd);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "Usage: /gmotd <message>";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /gpromote command
|
|
|
|
|
|
if (cmdLower == "gpromote" || cmdLower == "guildpromote") {
|
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
|
std::string playerName = command.substr(spacePos + 1);
|
|
|
|
|
|
gameHandler.promoteGuildMember(playerName);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "Usage: /gpromote <player>";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /gdemote command
|
|
|
|
|
|
if (cmdLower == "gdemote" || cmdLower == "guilddemote") {
|
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
|
std::string playerName = command.substr(spacePos + 1);
|
|
|
|
|
|
gameHandler.demoteGuildMember(playerName);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "Usage: /gdemote <player>";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /gquit command
|
|
|
|
|
|
if (cmdLower == "gquit" || cmdLower == "guildquit" || cmdLower == "leaveguild") {
|
|
|
|
|
|
gameHandler.leaveGuild();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /ginvite command
|
|
|
|
|
|
if (cmdLower == "ginvite" || cmdLower == "guildinvite") {
|
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
|
std::string playerName = command.substr(spacePos + 1);
|
|
|
|
|
|
gameHandler.inviteToGuild(playerName);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "Usage: /ginvite <player>";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 21:39:48 -08:00
|
|
|
|
// /gkick command
|
|
|
|
|
|
if (cmdLower == "gkick" || cmdLower == "guildkick") {
|
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
|
std::string playerName = command.substr(spacePos + 1);
|
|
|
|
|
|
gameHandler.kickGuildMember(playerName);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "Usage: /gkick <player>";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 14:44:44 -08:00
|
|
|
|
// /gcreate command
|
|
|
|
|
|
if (cmdLower == "gcreate" || cmdLower == "guildcreate") {
|
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
|
std::string guildName = command.substr(spacePos + 1);
|
|
|
|
|
|
gameHandler.createGuild(guildName);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "Usage: /gcreate <guild name>";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-16 20:16:14 -08:00
|
|
|
|
// /gdisband command
|
|
|
|
|
|
if (cmdLower == "gdisband" || cmdLower == "guilddisband") {
|
|
|
|
|
|
gameHandler.disbandGuild();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /gleader command
|
|
|
|
|
|
if (cmdLower == "gleader" || cmdLower == "guildleader") {
|
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
|
std::string playerName = command.substr(spacePos + 1);
|
|
|
|
|
|
gameHandler.setGuildLeader(playerName);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "Usage: /gleader <player>";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
// /readycheck command
|
|
|
|
|
|
if (cmdLower == "readycheck" || cmdLower == "rc") {
|
|
|
|
|
|
gameHandler.initiateReadyCheck();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /ready command (respond yes to ready check)
|
|
|
|
|
|
if (cmdLower == "ready") {
|
|
|
|
|
|
gameHandler.respondToReadyCheck(true);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /notready command (respond no to ready check)
|
|
|
|
|
|
if (cmdLower == "notready" || cmdLower == "nr") {
|
|
|
|
|
|
gameHandler.respondToReadyCheck(false);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /yield or /forfeit command
|
|
|
|
|
|
if (cmdLower == "yield" || cmdLower == "forfeit" || cmdLower == "surrender") {
|
|
|
|
|
|
gameHandler.forfeitDuel();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 13:17:01 -08:00
|
|
|
|
// AFK command
|
|
|
|
|
|
if (cmdLower == "afk" || cmdLower == "away") {
|
|
|
|
|
|
std::string afkMsg = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : "";
|
|
|
|
|
|
gameHandler.toggleAfk(afkMsg);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// DND command
|
|
|
|
|
|
if (cmdLower == "dnd" || cmdLower == "busy") {
|
|
|
|
|
|
std::string dndMsg = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : "";
|
|
|
|
|
|
gameHandler.toggleDnd(dndMsg);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Reply command
|
|
|
|
|
|
if (cmdLower == "r" || cmdLower == "reply") {
|
2026-02-16 20:16:14 -08:00
|
|
|
|
std::string lastSender = gameHandler.getLastWhisperSender();
|
|
|
|
|
|
if (lastSender.empty()) {
|
|
|
|
|
|
game::MessageChatData errMsg;
|
|
|
|
|
|
errMsg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
errMsg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
errMsg.message = "No one has whispered you yet.";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(errMsg);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Set whisper target to last whisper sender
|
|
|
|
|
|
strncpy(whisperTargetBuffer, lastSender.c_str(), sizeof(whisperTargetBuffer) - 1);
|
|
|
|
|
|
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
|
// /r message — send reply immediately
|
|
|
|
|
|
std::string replyMsg = command.substr(spacePos + 1);
|
|
|
|
|
|
gameHandler.sendChatMessage(game::ChatType::WHISPER, replyMsg, lastSender);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Switch to whisper tab
|
|
|
|
|
|
selectedChatType = 4;
|
2026-02-07 13:17:01 -08:00
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 13:28:46 -08:00
|
|
|
|
// Party/Raid management commands
|
|
|
|
|
|
if (cmdLower == "uninvite" || cmdLower == "kick") {
|
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
|
std::string playerName = command.substr(spacePos + 1);
|
|
|
|
|
|
gameHandler.uninvitePlayer(playerName);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "Usage: /uninvite <player name>";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "leave" || cmdLower == "leaveparty") {
|
|
|
|
|
|
gameHandler.leaveParty();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "maintank" || cmdLower == "mt") {
|
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
|
gameHandler.setMainTank(gameHandler.getTargetGuid());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "You must target a player to set as main tank.";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "mainassist" || cmdLower == "ma") {
|
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
|
gameHandler.setMainAssist(gameHandler.getTargetGuid());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "You must target a player to set as main assist.";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "clearmaintank") {
|
|
|
|
|
|
gameHandler.clearMainTank();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "clearmainassist") {
|
|
|
|
|
|
gameHandler.clearMainAssist();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "raidinfo") {
|
|
|
|
|
|
gameHandler.requestRaidInfo();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 13:36:50 -08:00
|
|
|
|
// Combat and Trade commands
|
|
|
|
|
|
if (cmdLower == "duel") {
|
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
|
gameHandler.proposeDuel(gameHandler.getTargetGuid());
|
|
|
|
|
|
} else if (spacePos != std::string::npos) {
|
|
|
|
|
|
// Target player by name (would need name-to-GUID lookup)
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "You must target a player to challenge to a duel.";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "You must target a player to challenge to a duel.";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "trade") {
|
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
|
gameHandler.initiateTrade(gameHandler.getTargetGuid());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "You must target a player to trade with.";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "startattack") {
|
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
|
gameHandler.startAutoAttack(gameHandler.getTargetGuid());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "You have no target.";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "stopattack") {
|
|
|
|
|
|
gameHandler.stopAutoAttack();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "stopcasting") {
|
|
|
|
|
|
gameHandler.stopCasting();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 09:30:59 -07:00
|
|
|
|
if (cmdLower == "cast" && spacePos != std::string::npos) {
|
|
|
|
|
|
std::string spellArg = command.substr(spacePos + 1);
|
|
|
|
|
|
// Trim leading/trailing whitespace
|
|
|
|
|
|
while (!spellArg.empty() && spellArg.front() == ' ') spellArg.erase(spellArg.begin());
|
|
|
|
|
|
while (!spellArg.empty() && spellArg.back() == ' ') spellArg.pop_back();
|
|
|
|
|
|
|
|
|
|
|
|
// Parse optional "(Rank N)" suffix: "Fireball(Rank 3)" or "Fireball (Rank 3)"
|
|
|
|
|
|
int requestedRank = -1; // -1 = highest rank
|
|
|
|
|
|
std::string spellName = spellArg;
|
|
|
|
|
|
{
|
|
|
|
|
|
auto rankPos = spellArg.find('(');
|
|
|
|
|
|
if (rankPos != std::string::npos) {
|
|
|
|
|
|
std::string rankStr = spellArg.substr(rankPos + 1);
|
|
|
|
|
|
// Strip closing paren and whitespace
|
|
|
|
|
|
auto closePos = rankStr.find(')');
|
|
|
|
|
|
if (closePos != std::string::npos) rankStr = rankStr.substr(0, closePos);
|
|
|
|
|
|
for (char& c : rankStr) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
// Expect "rank N"
|
|
|
|
|
|
if (rankStr.rfind("rank ", 0) == 0) {
|
|
|
|
|
|
try { requestedRank = std::stoi(rankStr.substr(5)); } catch (...) {}
|
|
|
|
|
|
}
|
|
|
|
|
|
spellName = spellArg.substr(0, rankPos);
|
|
|
|
|
|
while (!spellName.empty() && spellName.back() == ' ') spellName.pop_back();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string spellNameLower = spellName;
|
|
|
|
|
|
for (char& c : spellNameLower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
|
|
|
|
|
|
// Search known spells for a name match; pick highest rank (or specific rank)
|
|
|
|
|
|
uint32_t bestSpellId = 0;
|
|
|
|
|
|
int bestRank = -1;
|
|
|
|
|
|
for (uint32_t sid : gameHandler.getKnownSpells()) {
|
|
|
|
|
|
const std::string& sName = gameHandler.getSpellName(sid);
|
|
|
|
|
|
if (sName.empty()) continue;
|
|
|
|
|
|
std::string sNameLower = sName;
|
|
|
|
|
|
for (char& c : sNameLower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
if (sNameLower != spellNameLower) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// Parse numeric rank from rank string ("Rank 3" → 3, "" → 0)
|
|
|
|
|
|
int sRank = 0;
|
|
|
|
|
|
const std::string& rankStr = gameHandler.getSpellRank(sid);
|
|
|
|
|
|
if (!rankStr.empty()) {
|
|
|
|
|
|
std::string rLow = rankStr;
|
|
|
|
|
|
for (char& c : rLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
if (rLow.rfind("rank ", 0) == 0) {
|
|
|
|
|
|
try { sRank = std::stoi(rLow.substr(5)); } catch (...) {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (requestedRank >= 0) {
|
|
|
|
|
|
if (sRank == requestedRank) { bestSpellId = sid; break; }
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (sRank > bestRank) { bestRank = sRank; bestSpellId = sid; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (bestSpellId) {
|
|
|
|
|
|
uint64_t targetGuid = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
|
|
|
|
|
|
gameHandler.castSpell(bestSpellId, targetGuid);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
game::MessageChatData sysMsg;
|
|
|
|
|
|
sysMsg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
sysMsg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
sysMsg.message = requestedRank >= 0
|
|
|
|
|
|
? "You don't know '" + spellName + "' (Rank " + std::to_string(requestedRank) + ")."
|
|
|
|
|
|
: "Unknown spell: '" + spellName + "'.";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(sysMsg);
|
|
|
|
|
|
}
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 10:06:11 -07:00
|
|
|
|
// /use <item name> — use an item from backpack/bags by name
|
|
|
|
|
|
if (cmdLower == "use" && spacePos != std::string::npos) {
|
|
|
|
|
|
std::string useArg = command.substr(spacePos + 1);
|
|
|
|
|
|
while (!useArg.empty() && useArg.front() == ' ') useArg.erase(useArg.begin());
|
|
|
|
|
|
while (!useArg.empty() && useArg.back() == ' ') useArg.pop_back();
|
|
|
|
|
|
std::string useArgLower = useArg;
|
|
|
|
|
|
for (char& c : useArgLower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
|
|
|
|
|
|
bool found = false;
|
|
|
|
|
|
const auto& inv = gameHandler.getInventory();
|
|
|
|
|
|
// Search backpack
|
|
|
|
|
|
for (int s = 0; s < inv.getBackpackSize() && !found; ++s) {
|
|
|
|
|
|
const auto& slot = inv.getBackpackSlot(s);
|
|
|
|
|
|
if (slot.empty()) continue;
|
|
|
|
|
|
const auto* info = gameHandler.getItemInfo(slot.item.itemId);
|
|
|
|
|
|
if (!info) continue;
|
|
|
|
|
|
std::string nameLow = info->name;
|
|
|
|
|
|
for (char& c : nameLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
if (nameLow == useArgLower) {
|
|
|
|
|
|
gameHandler.useItemBySlot(s);
|
|
|
|
|
|
found = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Search bags
|
|
|
|
|
|
for (int b = 0; b < game::Inventory::NUM_BAG_SLOTS && !found; ++b) {
|
|
|
|
|
|
for (int s = 0; s < inv.getBagSize(b) && !found; ++s) {
|
|
|
|
|
|
const auto& slot = inv.getBagSlot(b, s);
|
|
|
|
|
|
if (slot.empty()) continue;
|
|
|
|
|
|
const auto* info = gameHandler.getItemInfo(slot.item.itemId);
|
|
|
|
|
|
if (!info) continue;
|
|
|
|
|
|
std::string nameLow = info->name;
|
|
|
|
|
|
for (char& c : nameLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
if (nameLow == useArgLower) {
|
|
|
|
|
|
gameHandler.useItemInBag(b, s);
|
|
|
|
|
|
found = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!found) {
|
|
|
|
|
|
game::MessageChatData sysMsg;
|
|
|
|
|
|
sysMsg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
sysMsg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
sysMsg.message = "Item not found: '" + useArg + "'.";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(sysMsg);
|
|
|
|
|
|
}
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /equip <item name> — auto-equip an item from backpack/bags by name
|
|
|
|
|
|
if (cmdLower == "equip" && spacePos != std::string::npos) {
|
|
|
|
|
|
std::string equipArg = command.substr(spacePos + 1);
|
|
|
|
|
|
while (!equipArg.empty() && equipArg.front() == ' ') equipArg.erase(equipArg.begin());
|
|
|
|
|
|
while (!equipArg.empty() && equipArg.back() == ' ') equipArg.pop_back();
|
|
|
|
|
|
std::string equipArgLower = equipArg;
|
|
|
|
|
|
for (char& c : equipArgLower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
|
|
|
|
|
|
bool found = false;
|
|
|
|
|
|
const auto& inv = gameHandler.getInventory();
|
|
|
|
|
|
// Search backpack
|
|
|
|
|
|
for (int s = 0; s < inv.getBackpackSize() && !found; ++s) {
|
|
|
|
|
|
const auto& slot = inv.getBackpackSlot(s);
|
|
|
|
|
|
if (slot.empty()) continue;
|
|
|
|
|
|
const auto* info = gameHandler.getItemInfo(slot.item.itemId);
|
|
|
|
|
|
if (!info) continue;
|
|
|
|
|
|
std::string nameLow = info->name;
|
|
|
|
|
|
for (char& c : nameLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
if (nameLow == equipArgLower) {
|
|
|
|
|
|
gameHandler.autoEquipItemBySlot(s);
|
|
|
|
|
|
found = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Search bags
|
|
|
|
|
|
for (int b = 0; b < game::Inventory::NUM_BAG_SLOTS && !found; ++b) {
|
|
|
|
|
|
for (int s = 0; s < inv.getBagSize(b) && !found; ++s) {
|
|
|
|
|
|
const auto& slot = inv.getBagSlot(b, s);
|
|
|
|
|
|
if (slot.empty()) continue;
|
|
|
|
|
|
const auto* info = gameHandler.getItemInfo(slot.item.itemId);
|
|
|
|
|
|
if (!info) continue;
|
|
|
|
|
|
std::string nameLow = info->name;
|
|
|
|
|
|
for (char& c : nameLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
if (nameLow == equipArgLower) {
|
|
|
|
|
|
gameHandler.autoEquipItemInBag(b, s);
|
|
|
|
|
|
found = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!found) {
|
|
|
|
|
|
game::MessageChatData sysMsg;
|
|
|
|
|
|
sysMsg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
sysMsg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
sysMsg.message = "Item not found: '" + equipArg + "'.";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(sysMsg);
|
|
|
|
|
|
}
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 13:44:36 -08:00
|
|
|
|
// Targeting commands
|
|
|
|
|
|
if (cmdLower == "cleartarget") {
|
|
|
|
|
|
gameHandler.clearTarget();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 22:57:04 -07:00
|
|
|
|
if (cmdLower == "target" && spacePos != std::string::npos) {
|
2026-03-18 00:39:32 -07:00
|
|
|
|
// Search visible entities for name match (case-insensitive prefix).
|
|
|
|
|
|
// Among all matches, pick the nearest living unit to the player.
|
2026-03-11 22:57:04 -07:00
|
|
|
|
std::string targetArg = command.substr(spacePos + 1);
|
|
|
|
|
|
std::string targetArgLower = targetArg;
|
|
|
|
|
|
for (char& c : targetArgLower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
uint64_t bestGuid = 0;
|
2026-03-18 00:39:32 -07:00
|
|
|
|
float bestDist = std::numeric_limits<float>::max();
|
|
|
|
|
|
const auto& pmi = gameHandler.getMovementInfo();
|
|
|
|
|
|
const float playerX = pmi.x;
|
|
|
|
|
|
const float playerY = pmi.y;
|
|
|
|
|
|
const float playerZ = pmi.z;
|
2026-03-11 22:57:04 -07:00
|
|
|
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
|
|
|
|
|
if (!entity || entity->getType() == game::ObjectType::OBJECT) continue;
|
|
|
|
|
|
std::string name;
|
|
|
|
|
|
if (entity->getType() == game::ObjectType::PLAYER ||
|
|
|
|
|
|
entity->getType() == game::ObjectType::UNIT) {
|
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
|
|
|
|
|
name = unit->getName();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (name.empty()) continue;
|
|
|
|
|
|
std::string nameLower = name;
|
|
|
|
|
|
for (char& c : nameLower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
if (nameLower.find(targetArgLower) == 0) {
|
2026-03-18 00:39:32 -07:00
|
|
|
|
float dx = entity->getX() - playerX;
|
|
|
|
|
|
float dy = entity->getY() - playerY;
|
|
|
|
|
|
float dz = entity->getZ() - playerZ;
|
|
|
|
|
|
float dist = dx*dx + dy*dy + dz*dz;
|
|
|
|
|
|
if (dist < bestDist) {
|
|
|
|
|
|
bestDist = dist;
|
|
|
|
|
|
bestGuid = guid;
|
|
|
|
|
|
}
|
2026-03-11 22:57:04 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (bestGuid) {
|
|
|
|
|
|
gameHandler.setTarget(bestGuid);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
game::MessageChatData sysMsg;
|
|
|
|
|
|
sysMsg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
sysMsg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
sysMsg.message = "No target matching '" + targetArg + "' found.";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(sysMsg);
|
|
|
|
|
|
}
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 13:44:36 -08:00
|
|
|
|
if (cmdLower == "targetenemy") {
|
|
|
|
|
|
gameHandler.targetEnemy(false);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "targetfriend") {
|
|
|
|
|
|
gameHandler.targetFriend(false);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "targetlasttarget" || cmdLower == "targetlast") {
|
|
|
|
|
|
gameHandler.targetLastTarget();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "targetlastenemy") {
|
|
|
|
|
|
gameHandler.targetEnemy(true); // Reverse direction
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "targetlastfriend") {
|
|
|
|
|
|
gameHandler.targetFriend(true); // Reverse direction
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "focus") {
|
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
|
gameHandler.setFocus(gameHandler.getTargetGuid());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "You must target a unit to set as focus.";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (cmdLower == "clearfocus") {
|
|
|
|
|
|
gameHandler.clearFocus();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 16:59:20 -08:00
|
|
|
|
// /unstuck command — resets player position to floor height
|
|
|
|
|
|
if (cmdLower == "unstuck") {
|
|
|
|
|
|
gameHandler.unstuck();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-08 03:24:12 -08:00
|
|
|
|
// /unstuckgy command — move to nearest graveyard
|
|
|
|
|
|
if (cmdLower == "unstuckgy") {
|
|
|
|
|
|
gameHandler.unstuckGy();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
2026-02-10 21:29:10 -08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-03-07 22:03:28 -08:00
|
|
|
|
// /unstuckhearth command — teleport to hearthstone bind point
|
|
|
|
|
|
if (cmdLower == "unstuckhearth") {
|
|
|
|
|
|
gameHandler.unstuckHearth();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-10 21:29:10 -08:00
|
|
|
|
|
|
|
|
|
|
// /transport board — board test transport
|
|
|
|
|
|
if (cmdLower == "transport board") {
|
|
|
|
|
|
auto* tm = gameHandler.getTransportManager();
|
|
|
|
|
|
if (tm) {
|
|
|
|
|
|
// Test transport GUID
|
|
|
|
|
|
uint64_t testTransportGuid = 0x1000000000000001ULL;
|
|
|
|
|
|
// Place player at center of deck (rough estimate)
|
|
|
|
|
|
glm::vec3 deckCenter(0.0f, 0.0f, 5.0f);
|
|
|
|
|
|
gameHandler.setPlayerOnTransport(testTransportGuid, deckCenter);
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "Boarded test transport. Use '/transport leave' to disembark.";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "Transport system not available.";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /transport leave — disembark from transport
|
|
|
|
|
|
if (cmdLower == "transport leave") {
|
|
|
|
|
|
if (gameHandler.isOnTransport()) {
|
|
|
|
|
|
gameHandler.clearPlayerTransport();
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "Disembarked from transport.";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "You are not on a transport.";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
2026-02-08 03:24:12 -08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-07 16:59:20 -08:00
|
|
|
|
|
2026-02-07 12:30:36 -08:00
|
|
|
|
// Chat channel slash commands
|
2026-02-07 23:32:27 -08:00
|
|
|
|
// If used without a message (e.g. just "/s"), switch the chat type dropdown
|
2026-02-07 12:30:36 -08:00
|
|
|
|
bool isChannelCommand = false;
|
|
|
|
|
|
if (cmdLower == "s" || cmdLower == "say") {
|
|
|
|
|
|
type = game::ChatType::SAY;
|
|
|
|
|
|
message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : "";
|
|
|
|
|
|
isChannelCommand = true;
|
2026-02-07 23:32:27 -08:00
|
|
|
|
switchChatType = 0;
|
|
|
|
|
|
} else if (cmdLower == "y" || cmdLower == "yell" || cmdLower == "shout") {
|
2026-02-07 12:30:36 -08:00
|
|
|
|
type = game::ChatType::YELL;
|
|
|
|
|
|
message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : "";
|
|
|
|
|
|
isChannelCommand = true;
|
2026-02-07 23:32:27 -08:00
|
|
|
|
switchChatType = 1;
|
2026-02-07 12:30:36 -08:00
|
|
|
|
} else if (cmdLower == "p" || cmdLower == "party") {
|
|
|
|
|
|
type = game::ChatType::PARTY;
|
|
|
|
|
|
message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : "";
|
|
|
|
|
|
isChannelCommand = true;
|
2026-02-07 23:32:27 -08:00
|
|
|
|
switchChatType = 2;
|
2026-02-07 12:30:36 -08:00
|
|
|
|
} else if (cmdLower == "g" || cmdLower == "guild") {
|
|
|
|
|
|
type = game::ChatType::GUILD;
|
|
|
|
|
|
message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : "";
|
|
|
|
|
|
isChannelCommand = true;
|
2026-02-07 23:32:27 -08:00
|
|
|
|
switchChatType = 3;
|
Add Tier 5 commands: raid, officer, and battleground chat channels
- Raid chat: /raid, /rsay, /ra to send messages to raid
- Raid warning: /raidwarning, /rw to send raid warnings (leader/assist only)
- Officer chat: /officer, /o, /osay to send messages to officer channel
- Battleground chat: /battleground, /bg to send messages in battlegrounds
- Instance chat: /instance, /i to send messages to instance party (uses PARTY type)
- Added all new chat types to dropdown selector
- Added color coding for RAID_WARNING (red), RAID_LEADER, BATTLEGROUND, and BATTLEGROUND_LEADER
- Added chat type names for proper display in chat log
2026-02-07 13:21:15 -08:00
|
|
|
|
} else if (cmdLower == "raid" || cmdLower == "rsay" || cmdLower == "ra") {
|
|
|
|
|
|
type = game::ChatType::RAID;
|
|
|
|
|
|
message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : "";
|
|
|
|
|
|
isChannelCommand = true;
|
2026-02-07 23:32:27 -08:00
|
|
|
|
switchChatType = 5;
|
Add Tier 5 commands: raid, officer, and battleground chat channels
- Raid chat: /raid, /rsay, /ra to send messages to raid
- Raid warning: /raidwarning, /rw to send raid warnings (leader/assist only)
- Officer chat: /officer, /o, /osay to send messages to officer channel
- Battleground chat: /battleground, /bg to send messages in battlegrounds
- Instance chat: /instance, /i to send messages to instance party (uses PARTY type)
- Added all new chat types to dropdown selector
- Added color coding for RAID_WARNING (red), RAID_LEADER, BATTLEGROUND, and BATTLEGROUND_LEADER
- Added chat type names for proper display in chat log
2026-02-07 13:21:15 -08:00
|
|
|
|
} else if (cmdLower == "raidwarning" || cmdLower == "rw") {
|
|
|
|
|
|
type = game::ChatType::RAID_WARNING;
|
|
|
|
|
|
message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : "";
|
|
|
|
|
|
isChannelCommand = true;
|
2026-02-07 23:32:27 -08:00
|
|
|
|
switchChatType = 8;
|
Add Tier 5 commands: raid, officer, and battleground chat channels
- Raid chat: /raid, /rsay, /ra to send messages to raid
- Raid warning: /raidwarning, /rw to send raid warnings (leader/assist only)
- Officer chat: /officer, /o, /osay to send messages to officer channel
- Battleground chat: /battleground, /bg to send messages in battlegrounds
- Instance chat: /instance, /i to send messages to instance party (uses PARTY type)
- Added all new chat types to dropdown selector
- Added color coding for RAID_WARNING (red), RAID_LEADER, BATTLEGROUND, and BATTLEGROUND_LEADER
- Added chat type names for proper display in chat log
2026-02-07 13:21:15 -08:00
|
|
|
|
} else if (cmdLower == "officer" || cmdLower == "o" || cmdLower == "osay") {
|
|
|
|
|
|
type = game::ChatType::OFFICER;
|
|
|
|
|
|
message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : "";
|
|
|
|
|
|
isChannelCommand = true;
|
2026-02-07 23:32:27 -08:00
|
|
|
|
switchChatType = 6;
|
Add Tier 5 commands: raid, officer, and battleground chat channels
- Raid chat: /raid, /rsay, /ra to send messages to raid
- Raid warning: /raidwarning, /rw to send raid warnings (leader/assist only)
- Officer chat: /officer, /o, /osay to send messages to officer channel
- Battleground chat: /battleground, /bg to send messages in battlegrounds
- Instance chat: /instance, /i to send messages to instance party (uses PARTY type)
- Added all new chat types to dropdown selector
- Added color coding for RAID_WARNING (red), RAID_LEADER, BATTLEGROUND, and BATTLEGROUND_LEADER
- Added chat type names for proper display in chat log
2026-02-07 13:21:15 -08:00
|
|
|
|
} else if (cmdLower == "battleground" || cmdLower == "bg") {
|
|
|
|
|
|
type = game::ChatType::BATTLEGROUND;
|
|
|
|
|
|
message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : "";
|
|
|
|
|
|
isChannelCommand = true;
|
2026-02-07 23:32:27 -08:00
|
|
|
|
switchChatType = 7;
|
Add Tier 5 commands: raid, officer, and battleground chat channels
- Raid chat: /raid, /rsay, /ra to send messages to raid
- Raid warning: /raidwarning, /rw to send raid warnings (leader/assist only)
- Officer chat: /officer, /o, /osay to send messages to officer channel
- Battleground chat: /battleground, /bg to send messages in battlegrounds
- Instance chat: /instance, /i to send messages to instance party (uses PARTY type)
- Added all new chat types to dropdown selector
- Added color coding for RAID_WARNING (red), RAID_LEADER, BATTLEGROUND, and BATTLEGROUND_LEADER
- Added chat type names for proper display in chat log
2026-02-07 13:21:15 -08:00
|
|
|
|
} else if (cmdLower == "instance" || cmdLower == "i") {
|
|
|
|
|
|
// Instance chat uses PARTY chat type
|
|
|
|
|
|
type = game::ChatType::PARTY;
|
|
|
|
|
|
message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : "";
|
|
|
|
|
|
isChannelCommand = true;
|
2026-02-07 23:32:27 -08:00
|
|
|
|
switchChatType = 9;
|
2026-02-14 14:30:09 -08:00
|
|
|
|
} else if (cmdLower == "join") {
|
2026-02-26 17:56:11 -08:00
|
|
|
|
// /join with no args: accept pending BG invite if any
|
|
|
|
|
|
if (spacePos == std::string::npos && gameHandler.hasPendingBgInvite()) {
|
|
|
|
|
|
gameHandler.acceptBattlefield();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-14 14:30:09 -08:00
|
|
|
|
// /join ChannelName [password]
|
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
|
std::string rest = command.substr(spacePos + 1);
|
|
|
|
|
|
size_t pwStart = rest.find(' ');
|
|
|
|
|
|
std::string channelName = (pwStart != std::string::npos) ? rest.substr(0, pwStart) : rest;
|
|
|
|
|
|
std::string password = (pwStart != std::string::npos) ? rest.substr(pwStart + 1) : "";
|
|
|
|
|
|
gameHandler.joinChannel(channelName, password);
|
|
|
|
|
|
}
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
} else if (cmdLower == "leave") {
|
|
|
|
|
|
// /leave ChannelName
|
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
|
std::string channelName = command.substr(spacePos + 1);
|
|
|
|
|
|
gameHandler.leaveChannel(channelName);
|
|
|
|
|
|
}
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
2026-03-12 09:38:29 -07:00
|
|
|
|
} else if ((cmdLower == "wts" || cmdLower == "wtb") && spacePos != std::string::npos) {
|
|
|
|
|
|
// /wts and /wtb — send to Trade channel
|
|
|
|
|
|
// Prefix with [WTS] / [WTB] and route to the Trade channel
|
|
|
|
|
|
const std::string tag = (cmdLower == "wts") ? "[WTS] " : "[WTB] ";
|
|
|
|
|
|
const std::string body = command.substr(spacePos + 1);
|
|
|
|
|
|
// Find the Trade channel among joined channels (case-insensitive prefix match)
|
|
|
|
|
|
std::string tradeChan;
|
|
|
|
|
|
for (const auto& ch : gameHandler.getJoinedChannels()) {
|
|
|
|
|
|
std::string chLow = ch;
|
|
|
|
|
|
for (char& c : chLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
if (chLow.rfind("trade", 0) == 0) { tradeChan = ch; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (tradeChan.empty()) {
|
|
|
|
|
|
game::MessageChatData errMsg;
|
|
|
|
|
|
errMsg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
errMsg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
errMsg.message = "You are not in the Trade channel.";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(errMsg);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
message = tag + body;
|
|
|
|
|
|
type = game::ChatType::CHANNEL;
|
|
|
|
|
|
target = tradeChan;
|
|
|
|
|
|
isChannelCommand = true;
|
2026-02-14 14:30:09 -08:00
|
|
|
|
} else if (cmdLower.size() == 1 && cmdLower[0] >= '1' && cmdLower[0] <= '9') {
|
|
|
|
|
|
// /1 msg, /2 msg — channel shortcuts
|
|
|
|
|
|
int channelIdx = cmdLower[0] - '0';
|
|
|
|
|
|
std::string channelName = gameHandler.getChannelByIndex(channelIdx);
|
|
|
|
|
|
if (!channelName.empty() && spacePos != std::string::npos) {
|
|
|
|
|
|
message = command.substr(spacePos + 1);
|
|
|
|
|
|
type = game::ChatType::CHANNEL;
|
|
|
|
|
|
target = channelName;
|
|
|
|
|
|
isChannelCommand = true;
|
|
|
|
|
|
} else if (channelName.empty()) {
|
|
|
|
|
|
game::MessageChatData errMsg;
|
|
|
|
|
|
errMsg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
errMsg.message = "You are not in channel " + std::to_string(channelIdx) + ".";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(errMsg);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-07 12:30:36 -08:00
|
|
|
|
} else if (cmdLower == "w" || cmdLower == "whisper" || cmdLower == "tell" || cmdLower == "t") {
|
2026-02-07 23:32:27 -08:00
|
|
|
|
switchChatType = 4;
|
2026-02-07 12:30:36 -08:00
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
|
std::string rest = command.substr(spacePos + 1);
|
|
|
|
|
|
size_t msgStart = rest.find(' ');
|
|
|
|
|
|
if (msgStart != std::string::npos) {
|
2026-02-07 23:32:27 -08:00
|
|
|
|
// /w PlayerName message — send whisper immediately
|
2026-02-07 12:30:36 -08:00
|
|
|
|
target = rest.substr(0, msgStart);
|
|
|
|
|
|
message = rest.substr(msgStart + 1);
|
|
|
|
|
|
type = game::ChatType::WHISPER;
|
|
|
|
|
|
isChannelCommand = true;
|
2026-02-07 23:32:27 -08:00
|
|
|
|
// Set whisper target for future messages
|
|
|
|
|
|
strncpy(whisperTargetBuffer, target.c_str(), sizeof(whisperTargetBuffer) - 1);
|
|
|
|
|
|
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
2026-02-02 12:24:50 -08:00
|
|
|
|
} else {
|
2026-02-07 23:32:27 -08:00
|
|
|
|
// /w PlayerName — switch to whisper mode with target set
|
|
|
|
|
|
strncpy(whisperTargetBuffer, rest.c_str(), sizeof(whisperTargetBuffer) - 1);
|
|
|
|
|
|
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
|
|
|
|
|
message = "";
|
|
|
|
|
|
isChannelCommand = true;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2026-02-07 23:32:27 -08:00
|
|
|
|
// Just "/w" — switch to whisper mode
|
|
|
|
|
|
message = "";
|
|
|
|
|
|
isChannelCommand = true;
|
2026-02-07 12:30:36 -08:00
|
|
|
|
}
|
2026-03-17 10:12:49 -07:00
|
|
|
|
} else if (cmdLower == "r" || cmdLower == "reply") {
|
|
|
|
|
|
switchChatType = 4;
|
|
|
|
|
|
std::string lastSender = gameHandler.getLastWhisperSender();
|
|
|
|
|
|
if (lastSender.empty()) {
|
|
|
|
|
|
game::MessageChatData sysMsg;
|
|
|
|
|
|
sysMsg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
sysMsg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
sysMsg.message = "No one has whispered you yet.";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(sysMsg);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
target = lastSender;
|
|
|
|
|
|
strncpy(whisperTargetBuffer, target.c_str(), sizeof(whisperTargetBuffer) - 1);
|
|
|
|
|
|
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
|
|
|
|
|
if (spacePos != std::string::npos) {
|
|
|
|
|
|
message = command.substr(spacePos + 1);
|
|
|
|
|
|
type = game::ChatType::WHISPER;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message = "";
|
|
|
|
|
|
}
|
|
|
|
|
|
isChannelCommand = true;
|
2026-02-07 12:30:36 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check for emote commands
|
|
|
|
|
|
if (!isChannelCommand) {
|
2026-02-07 20:02:14 -08:00
|
|
|
|
std::string targetName;
|
|
|
|
|
|
const std::string* targetNamePtr = nullptr;
|
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
|
auto targetEntity = gameHandler.getTarget();
|
|
|
|
|
|
if (targetEntity) {
|
|
|
|
|
|
targetName = getEntityName(targetEntity);
|
|
|
|
|
|
if (!targetName.empty()) targetNamePtr = &targetName;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string emoteText = rendering::Renderer::getEmoteText(cmdLower, targetNamePtr);
|
2026-02-07 12:30:36 -08:00
|
|
|
|
if (!emoteText.empty()) {
|
|
|
|
|
|
// Play the emote animation
|
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
renderer->playEmote(cmdLower);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 14:30:09 -08:00
|
|
|
|
// Send CMSG_TEXT_EMOTE to server
|
|
|
|
|
|
uint32_t dbcId = rendering::Renderer::getEmoteDbcId(cmdLower);
|
|
|
|
|
|
if (dbcId != 0) {
|
|
|
|
|
|
uint64_t targetGuid = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
|
|
|
|
|
|
gameHandler.sendTextEmote(dbcId, targetGuid);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 12:30:36 -08:00
|
|
|
|
// Add local chat message
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::TEXT_EMOTE;
|
|
|
|
|
|
msg.language = game::ChatLanguage::COMMON;
|
2026-02-07 20:02:14 -08:00
|
|
|
|
msg.message = emoteText;
|
2026-02-07 12:30:36 -08:00
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 12:30:36 -08:00
|
|
|
|
// Not a recognized command — fall through and send as normal chat
|
|
|
|
|
|
if (!isChannelCommand) {
|
|
|
|
|
|
message = input;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-07 12:30:36 -08:00
|
|
|
|
// If no valid command found and starts with /, just send as-is
|
|
|
|
|
|
if (!isChannelCommand && message == input) {
|
|
|
|
|
|
// Use the selected chat type from dropdown
|
|
|
|
|
|
switch (selectedChatType) {
|
|
|
|
|
|
case 0: type = game::ChatType::SAY; break;
|
|
|
|
|
|
case 1: type = game::ChatType::YELL; break;
|
|
|
|
|
|
case 2: type = game::ChatType::PARTY; break;
|
|
|
|
|
|
case 3: type = game::ChatType::GUILD; break;
|
|
|
|
|
|
case 4: type = game::ChatType::WHISPER; target = whisperTargetBuffer; break;
|
Add Tier 5 commands: raid, officer, and battleground chat channels
- Raid chat: /raid, /rsay, /ra to send messages to raid
- Raid warning: /raidwarning, /rw to send raid warnings (leader/assist only)
- Officer chat: /officer, /o, /osay to send messages to officer channel
- Battleground chat: /battleground, /bg to send messages in battlegrounds
- Instance chat: /instance, /i to send messages to instance party (uses PARTY type)
- Added all new chat types to dropdown selector
- Added color coding for RAID_WARNING (red), RAID_LEADER, BATTLEGROUND, and BATTLEGROUND_LEADER
- Added chat type names for proper display in chat log
2026-02-07 13:21:15 -08:00
|
|
|
|
case 5: type = game::ChatType::RAID; break;
|
|
|
|
|
|
case 6: type = game::ChatType::OFFICER; break;
|
|
|
|
|
|
case 7: type = game::ChatType::BATTLEGROUND; break;
|
|
|
|
|
|
case 8: type = game::ChatType::RAID_WARNING; break;
|
|
|
|
|
|
case 9: type = game::ChatType::PARTY; break; // INSTANCE uses PARTY
|
2026-03-12 11:16:42 -07:00
|
|
|
|
case 10: { // CHANNEL
|
|
|
|
|
|
const auto& chans = gameHandler.getJoinedChannels();
|
|
|
|
|
|
if (!chans.empty() && selectedChannelIdx < static_cast<int>(chans.size())) {
|
|
|
|
|
|
type = game::ChatType::CHANNEL;
|
|
|
|
|
|
target = chans[selectedChannelIdx];
|
|
|
|
|
|
} else { type = game::ChatType::SAY; }
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2026-02-07 12:30:36 -08:00
|
|
|
|
default: type = game::ChatType::SAY; break;
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
2026-02-07 12:30:36 -08:00
|
|
|
|
} else {
|
|
|
|
|
|
// No slash command, use the selected chat type from dropdown
|
|
|
|
|
|
switch (selectedChatType) {
|
|
|
|
|
|
case 0: type = game::ChatType::SAY; break;
|
|
|
|
|
|
case 1: type = game::ChatType::YELL; break;
|
|
|
|
|
|
case 2: type = game::ChatType::PARTY; break;
|
|
|
|
|
|
case 3: type = game::ChatType::GUILD; break;
|
|
|
|
|
|
case 4: type = game::ChatType::WHISPER; target = whisperTargetBuffer; break;
|
Add Tier 5 commands: raid, officer, and battleground chat channels
- Raid chat: /raid, /rsay, /ra to send messages to raid
- Raid warning: /raidwarning, /rw to send raid warnings (leader/assist only)
- Officer chat: /officer, /o, /osay to send messages to officer channel
- Battleground chat: /battleground, /bg to send messages in battlegrounds
- Instance chat: /instance, /i to send messages to instance party (uses PARTY type)
- Added all new chat types to dropdown selector
- Added color coding for RAID_WARNING (red), RAID_LEADER, BATTLEGROUND, and BATTLEGROUND_LEADER
- Added chat type names for proper display in chat log
2026-02-07 13:21:15 -08:00
|
|
|
|
case 5: type = game::ChatType::RAID; break;
|
|
|
|
|
|
case 6: type = game::ChatType::OFFICER; break;
|
|
|
|
|
|
case 7: type = game::ChatType::BATTLEGROUND; break;
|
|
|
|
|
|
case 8: type = game::ChatType::RAID_WARNING; break;
|
|
|
|
|
|
case 9: type = game::ChatType::PARTY; break; // INSTANCE uses PARTY
|
2026-03-12 11:16:42 -07:00
|
|
|
|
case 10: { // CHANNEL
|
|
|
|
|
|
const auto& chans = gameHandler.getJoinedChannels();
|
|
|
|
|
|
if (!chans.empty() && selectedChannelIdx < static_cast<int>(chans.size())) {
|
|
|
|
|
|
type = game::ChatType::CHANNEL;
|
|
|
|
|
|
target = chans[selectedChannelIdx];
|
|
|
|
|
|
} else { type = game::ChatType::SAY; }
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2026-02-07 12:30:36 -08:00
|
|
|
|
default: type = game::ChatType::SAY; break;
|
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-02-11 21:14:35 -08:00
|
|
|
|
// Whisper shortcuts to PortBot/GMBot: translate to GM teleport commands.
|
|
|
|
|
|
if (type == game::ChatType::WHISPER && isPortBotTarget(target)) {
|
|
|
|
|
|
std::string cmd = buildPortBotCommand(message);
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
if (cmd.empty() || cmd == "__help__") {
|
|
|
|
|
|
msg.message = "PortBot: /w PortBot <dest>. Aliases: sw if darn org tb uc shatt dal. Also supports '.tele ...' or 'xyz x y z [map [o]]'.";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
gameHandler.sendChatMessage(game::ChatType::SAY, cmd, "");
|
|
|
|
|
|
msg.message = "PortBot executed: " + cmd;
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 12:30:36 -08:00
|
|
|
|
// Validate whisper has a target
|
|
|
|
|
|
if (type == game::ChatType::WHISPER && target.empty()) {
|
|
|
|
|
|
game::MessageChatData msg;
|
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
msg.message = "You must specify a player name for whisper.";
|
|
|
|
|
|
gameHandler.addLocalChatMessage(msg);
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 23:32:27 -08:00
|
|
|
|
// Don't send empty messages — but switch chat type if a channel shortcut was used
|
2026-02-07 12:30:36 -08:00
|
|
|
|
if (!message.empty()) {
|
|
|
|
|
|
gameHandler.sendChatMessage(type, message, target);
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-07 23:32:27 -08:00
|
|
|
|
// Switch chat type dropdown when channel shortcut used (with or without message)
|
|
|
|
|
|
if (switchChatType >= 0) {
|
|
|
|
|
|
selectedChatType = switchChatType;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
// Clear input
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const char* GameScreen::getChatTypeName(game::ChatType type) const {
|
|
|
|
|
|
switch (type) {
|
2026-03-10 15:08:21 -07:00
|
|
|
|
case game::ChatType::SAY: return "Say";
|
|
|
|
|
|
case game::ChatType::YELL: return "Yell";
|
|
|
|
|
|
case game::ChatType::EMOTE: return "Emote";
|
|
|
|
|
|
case game::ChatType::TEXT_EMOTE: return "Emote";
|
|
|
|
|
|
case game::ChatType::PARTY: return "Party";
|
|
|
|
|
|
case game::ChatType::GUILD: return "Guild";
|
|
|
|
|
|
case game::ChatType::OFFICER: return "Officer";
|
|
|
|
|
|
case game::ChatType::RAID: return "Raid";
|
|
|
|
|
|
case game::ChatType::RAID_LEADER: return "Raid Leader";
|
|
|
|
|
|
case game::ChatType::RAID_WARNING: return "Raid Warning";
|
|
|
|
|
|
case game::ChatType::BATTLEGROUND: return "Battleground";
|
|
|
|
|
|
case game::ChatType::BATTLEGROUND_LEADER: return "Battleground Leader";
|
|
|
|
|
|
case game::ChatType::WHISPER: return "Whisper";
|
|
|
|
|
|
case game::ChatType::WHISPER_INFORM: return "To";
|
|
|
|
|
|
case game::ChatType::SYSTEM: return "System";
|
|
|
|
|
|
case game::ChatType::MONSTER_SAY: return "Say";
|
|
|
|
|
|
case game::ChatType::MONSTER_YELL: return "Yell";
|
|
|
|
|
|
case game::ChatType::MONSTER_EMOTE: return "Emote";
|
|
|
|
|
|
case game::ChatType::CHANNEL: return "Channel";
|
|
|
|
|
|
case game::ChatType::ACHIEVEMENT: return "Achievement";
|
2026-02-13 18:59:09 -08:00
|
|
|
|
case game::ChatType::DND: return "DND";
|
|
|
|
|
|
case game::ChatType::AFK: return "AFK";
|
2026-03-10 15:32:04 -07:00
|
|
|
|
case game::ChatType::BG_SYSTEM_NEUTRAL:
|
|
|
|
|
|
case game::ChatType::BG_SYSTEM_ALLIANCE:
|
|
|
|
|
|
case game::ChatType::BG_SYSTEM_HORDE: return "System";
|
2026-03-10 15:08:21 -07:00
|
|
|
|
default: return "Unknown";
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImVec4 GameScreen::getChatTypeColor(game::ChatType type) const {
|
|
|
|
|
|
switch (type) {
|
|
|
|
|
|
case game::ChatType::SAY:
|
|
|
|
|
|
return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White
|
|
|
|
|
|
case game::ChatType::YELL:
|
|
|
|
|
|
return ImVec4(1.0f, 0.3f, 0.3f, 1.0f); // Red
|
|
|
|
|
|
case game::ChatType::EMOTE:
|
|
|
|
|
|
return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange
|
|
|
|
|
|
case game::ChatType::TEXT_EMOTE:
|
|
|
|
|
|
return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange
|
|
|
|
|
|
case game::ChatType::PARTY:
|
|
|
|
|
|
return ImVec4(0.5f, 0.5f, 1.0f, 1.0f); // Light blue
|
|
|
|
|
|
case game::ChatType::GUILD:
|
|
|
|
|
|
return ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Green
|
|
|
|
|
|
case game::ChatType::OFFICER:
|
|
|
|
|
|
return ImVec4(0.3f, 0.8f, 0.3f, 1.0f); // Dark green
|
|
|
|
|
|
case game::ChatType::RAID:
|
|
|
|
|
|
return ImVec4(1.0f, 0.5f, 0.0f, 1.0f); // Orange
|
Add Tier 5 commands: raid, officer, and battleground chat channels
- Raid chat: /raid, /rsay, /ra to send messages to raid
- Raid warning: /raidwarning, /rw to send raid warnings (leader/assist only)
- Officer chat: /officer, /o, /osay to send messages to officer channel
- Battleground chat: /battleground, /bg to send messages in battlegrounds
- Instance chat: /instance, /i to send messages to instance party (uses PARTY type)
- Added all new chat types to dropdown selector
- Added color coding for RAID_WARNING (red), RAID_LEADER, BATTLEGROUND, and BATTLEGROUND_LEADER
- Added chat type names for proper display in chat log
2026-02-07 13:21:15 -08:00
|
|
|
|
case game::ChatType::RAID_LEADER:
|
|
|
|
|
|
return ImVec4(1.0f, 0.4f, 0.0f, 1.0f); // Darker orange
|
|
|
|
|
|
case game::ChatType::RAID_WARNING:
|
|
|
|
|
|
return ImVec4(1.0f, 0.0f, 0.0f, 1.0f); // Red
|
|
|
|
|
|
case game::ChatType::BATTLEGROUND:
|
|
|
|
|
|
return ImVec4(1.0f, 0.6f, 0.0f, 1.0f); // Orange-gold
|
|
|
|
|
|
case game::ChatType::BATTLEGROUND_LEADER:
|
|
|
|
|
|
return ImVec4(1.0f, 0.5f, 0.0f, 1.0f); // Orange
|
2026-02-02 12:24:50 -08:00
|
|
|
|
case game::ChatType::WHISPER:
|
|
|
|
|
|
return ImVec4(1.0f, 0.5f, 1.0f, 1.0f); // Pink
|
|
|
|
|
|
case game::ChatType::WHISPER_INFORM:
|
|
|
|
|
|
return ImVec4(1.0f, 0.5f, 1.0f, 1.0f); // Pink
|
|
|
|
|
|
case game::ChatType::SYSTEM:
|
|
|
|
|
|
return ImVec4(1.0f, 1.0f, 0.3f, 1.0f); // Yellow
|
2026-02-13 18:59:09 -08:00
|
|
|
|
case game::ChatType::MONSTER_SAY:
|
|
|
|
|
|
return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White (same as SAY)
|
|
|
|
|
|
case game::ChatType::MONSTER_YELL:
|
|
|
|
|
|
return ImVec4(1.0f, 0.3f, 0.3f, 1.0f); // Red (same as YELL)
|
|
|
|
|
|
case game::ChatType::MONSTER_EMOTE:
|
|
|
|
|
|
return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange (same as EMOTE)
|
2026-02-02 12:24:50 -08:00
|
|
|
|
case game::ChatType::CHANNEL:
|
|
|
|
|
|
return ImVec4(1.0f, 0.7f, 0.7f, 1.0f); // Light pink
|
|
|
|
|
|
case game::ChatType::ACHIEVEMENT:
|
|
|
|
|
|
return ImVec4(1.0f, 1.0f, 0.0f, 1.0f); // Bright yellow
|
feat: add colors for SKILL, LOOT, BG system, and monster chat types
Added distinct colors for chat types that previously fell through to
the gray default: SKILL (cyan), LOOT (light purple), GUILD_ACHIEVEMENT
(gold), MONSTER_WHISPER/RAID_BOSS_WHISPER (pink), RAID_BOSS_EMOTE
(orange), MONSTER_PARTY (blue), BG_SYSTEM_NEUTRAL/ALLIANCE/HORDE
(gold/blue/red), and AFK/DND (light gray).
2026-03-17 17:00:46 -07:00
|
|
|
|
case game::ChatType::GUILD_ACHIEVEMENT:
|
|
|
|
|
|
return ImVec4(1.0f, 0.84f, 0.0f, 1.0f); // Gold
|
|
|
|
|
|
case game::ChatType::SKILL:
|
|
|
|
|
|
return ImVec4(0.0f, 0.8f, 1.0f, 1.0f); // Cyan
|
|
|
|
|
|
case game::ChatType::LOOT:
|
|
|
|
|
|
return ImVec4(0.8f, 0.5f, 1.0f, 1.0f); // Light purple
|
|
|
|
|
|
case game::ChatType::MONSTER_WHISPER:
|
|
|
|
|
|
case game::ChatType::RAID_BOSS_WHISPER:
|
|
|
|
|
|
return ImVec4(1.0f, 0.5f, 1.0f, 1.0f); // Pink (same as WHISPER)
|
|
|
|
|
|
case game::ChatType::RAID_BOSS_EMOTE:
|
|
|
|
|
|
return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange (same as EMOTE)
|
|
|
|
|
|
case game::ChatType::MONSTER_PARTY:
|
|
|
|
|
|
return ImVec4(0.5f, 0.5f, 1.0f, 1.0f); // Light blue (same as PARTY)
|
|
|
|
|
|
case game::ChatType::BG_SYSTEM_NEUTRAL:
|
|
|
|
|
|
return ImVec4(1.0f, 0.84f, 0.0f, 1.0f); // Gold
|
|
|
|
|
|
case game::ChatType::BG_SYSTEM_ALLIANCE:
|
|
|
|
|
|
return ImVec4(0.3f, 0.6f, 1.0f, 1.0f); // Blue
|
|
|
|
|
|
case game::ChatType::BG_SYSTEM_HORDE:
|
|
|
|
|
|
return ImVec4(1.0f, 0.3f, 0.3f, 1.0f); // Red
|
|
|
|
|
|
case game::ChatType::AFK:
|
|
|
|
|
|
case game::ChatType::DND:
|
|
|
|
|
|
return ImVec4(0.85f, 0.85f, 0.85f, 0.8f); // Light gray
|
2026-02-02 12:24:50 -08:00
|
|
|
|
default:
|
|
|
|
|
|
return ImVec4(0.7f, 0.7f, 0.7f, 1.0f); // Gray
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
static const char* componentDirs[] = {
|
|
|
|
|
|
"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-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
|
|
|
|
}
|
|
|
|
|
|
|
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-11 22:39:59 -07:00
|
|
|
|
float slotSize = 48.0f * 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();
|
|
|
|
|
|
static const char* keyLabels1[] = {"1","2","3","4","5","6","7","8","9","0","-","="};
|
|
|
|
|
|
// "⇧N" labels for bar 2 (UTF-8: E2 87 A7 = U+21E7 UPWARDS WHITE ARROW)
|
|
|
|
|
|
static const char* keyLabels2[] = {
|
|
|
|
|
|
"\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-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.
|
|
|
|
|
|
// Only applies to SPELL 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;
|
|
|
|
|
|
if (!slot.isEmpty() && slot.type == game::ActionBarSlot::SPELL && slot.id != 0
|
|
|
|
|
|
&& !onCooldown && gameHandler.hasTarget()) {
|
|
|
|
|
|
uint32_t maxRange = spellbookScreen.getSpellMaxRange(slot.id, assetMgr);
|
|
|
|
|
|
if (maxRange > 5) { // >5 yd = not melee/self
|
|
|
|
|
|
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();
|
|
|
|
|
|
float dist = std::sqrt(dx * dx + dy * dy + dz * dz);
|
|
|
|
|
|
if (dist > static_cast<float>(maxRange))
|
|
|
|
|
|
outOfRange = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 06:01:42 -07:00
|
|
|
|
// Insufficient-power check: orange tint when player doesn't have enough power to cast.
|
|
|
|
|
|
// Only applies to SPELL slots with a known power cost and when not already on cooldown.
|
|
|
|
|
|
bool insufficientPower = false;
|
|
|
|
|
|
if (!slot.isEmpty() && slot.type == game::ActionBarSlot::SPELL && slot.id != 0
|
|
|
|
|
|
&& !onCooldown) {
|
|
|
|
|
|
uint32_t spellCost = 0, spellPowerType = 0;
|
|
|
|
|
|
spellbookScreen.getSpellPowerInfo(slot.id, assetMgr, spellCost, spellPowerType);
|
|
|
|
|
|
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-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-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()) {
|
|
|
|
|
|
uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
|
|
|
|
|
|
gameHandler.castSpell(slot.id, target);
|
|
|
|
|
|
} else if (slot.type == game::ActionBarSlot::ITEM && slot.id != 0) {
|
|
|
|
|
|
gameHandler.useItemById(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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f), "Out of range");
|
|
|
|
|
|
}
|
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)
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
|
|
|
|
|
|
"Cooldown: %d min %d sec", (int)cd/60, (int)cd%60);
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Cooldown: %.1f sec", cd);
|
|
|
|
|
|
}
|
|
|
|
|
|
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)
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
|
|
|
|
|
|
"Cooldown: %d min %d sec", (int)cd/60, (int)cd%60);
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Cooldown: %.1f sec", cd);
|
|
|
|
|
|
}
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
|
|
float total = (slot.cooldownTotal > 0.0f) ? slot.cooldownTotal : 1.0f;
|
|
|
|
|
|
float elapsed = total - slot.cooldownRemaining;
|
|
|
|
|
|
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];
|
|
|
|
|
|
float cd = slot.cooldownRemaining;
|
2026-03-12 03:48:12 -07:00
|
|
|
|
if (cd >= 3600.0f) snprintf(cdText, sizeof(cdText), "%dh", (int)cd / 3600);
|
|
|
|
|
|
else if (cd >= 60.0f) snprintf(cdText, sizeof(cdText), "%dm%ds", (int)cd / 60, (int)cd % 60);
|
|
|
|
|
|
else if (cd >= 5.0f) snprintf(cdText, sizeof(cdText), "%ds", (int)cd);
|
|
|
|
|
|
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) {
|
|
|
|
|
|
char countStr[8];
|
|
|
|
|
|
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-10 15:45:35 -07:00
|
|
|
|
if (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-10 15:45:35 -07:00
|
|
|
|
float bar2X = barX + pendingActionBar2OffsetX;
|
|
|
|
|
|
float bar2Y = barY - barH - 2.0f + pendingActionBar2OffsetY;
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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)
|
|
|
|
|
|
if (pendingShowRightBar) {
|
|
|
|
|
|
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;
|
|
|
|
|
|
float sideBarY = (screenH - sideBarH) / 2.0f + pendingRightBarOffsetY;
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
if (pendingShowLeftBar) {
|
|
|
|
|
|
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;
|
|
|
|
|
|
float sideBarY = (screenH - sideBarH) / 2.0f + pendingLeftBarOffsetY;
|
|
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
|
|
|
|
|
|
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
|
|
|
|
|
|
float actionSlot = 48.0f * pendingActionBarScale;
|
|
|
|
|
|
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),
|
|
|
|
|
|
ImVec4(1, 1, 1, 1))) {
|
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-11 22:39:59 -07:00
|
|
|
|
float slotSize = 48.0f * 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;
|
|
|
|
|
|
if (pendingShowActionBar2) {
|
|
|
|
|
|
float bar2TopY = bar1TopY - barH - 2.0f + pendingActionBar2OffsetY;
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
float slotSize = 48.0f * pendingActionBarScale;
|
|
|
|
|
|
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;
|
|
|
|
|
|
if (pendingShowActionBar2) {
|
|
|
|
|
|
float bar2TopY = bar1TopY - barH_ab - 2.0f + pendingActionBar2OffsetY;
|
|
|
|
|
|
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-02-15 03:17:51 -08:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(barX, barY), ImGuiCond_FirstUseEver);
|
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 |
|
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar;
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
if (iconTex) {
|
|
|
|
|
|
// Spell icon to the left of the progress bar
|
|
|
|
|
|
ImGui::Image((ImTextureID)(uintptr_t)iconTex, ImVec2(20, 20));
|
|
|
|
|
|
ImGui::SameLine(0, 4);
|
|
|
|
|
|
ImGui::ProgressBar(progress, ImVec2(-1, 20), overlay);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::ProgressBar(progress, ImVec2(-1, 20), overlay);
|
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
|
|
static const struct { const char* label; ImVec4 color; } kTimerInfo[3] = {
|
|
|
|
|
|
{ "Fatigue", ImVec4(0.8f, 0.4f, 0.1f, 1.0f) },
|
|
|
|
|
|
{ "Breath", ImVec4(0.2f, 0.5f, 1.0f, 1.0f) },
|
|
|
|
|
|
{ "Feign", ImVec4(0.6f, 0.6f, 0.6f, 1.0f) },
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
|
if (!showCooldownTracker_) return;
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
snprintf(timeStr, sizeof(timeStr), "%dm%ds", (int)cd.remaining / 60, (int)cd.remaining % 60);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(timeStr, sizeof(timeStr), "%.0fs", cd.remaining);
|
|
|
|
|
|
|
|
|
|
|
|
// Color: red > 30s, orange > 10s, yellow > 5s, green otherwise
|
|
|
|
|
|
ImVec4 cdColor = cd.remaining > 30.0f ? ImVec4(1.0f, 0.3f, 0.3f, 1.0f) :
|
|
|
|
|
|
cd.remaining > 10.0f ? ImVec4(1.0f, 0.6f, 0.2f, 1.0f) :
|
|
|
|
|
|
cd.remaining > 5.0f ? ImVec4(1.0f, 1.0f, 0.3f, 1.0f) :
|
|
|
|
|
|
ImVec4(0.5f, 1.0f, 0.5f, 1.0f);
|
|
|
|
|
|
|
|
|
|
|
|
// 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-09 15:05:38 -07:00
|
|
|
|
ImVec4 titleCol = q.complete ? ImVec4(1.0f, 0.84f, 0.0f, 1.0f)
|
|
|
|
|
|
: 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) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.5f, 1.0f, 0.5f, 1.0f), " (Complete)");
|
|
|
|
|
|
} 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);
|
|
|
|
|
|
ImVec4 objColor = objDone ? ImVec4(0.4f, 1.0f, 0.4f, 1.0f)
|
|
|
|
|
|
: 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);
|
|
|
|
|
|
ImVec4 objColor = objDone ? ImVec4(0.4f, 1.0f, 0.4f, 1.0f)
|
|
|
|
|
|
: 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();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
|
|
|
|
|
|
// Render combat text entries overlaid on screen
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(0, 0));
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(screenW, 400));
|
|
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration |
|
|
|
|
|
|
ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav;
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##CombatText", nullptr, flags)) {
|
2026-02-17 15:24:39 -08:00
|
|
|
|
// Incoming events (enemy attacks player) float near screen center (over the player).
|
|
|
|
|
|
// Outgoing events (player attacks enemy) float on the right side (near the target).
|
|
|
|
|
|
const float incomingX = screenW * 0.40f;
|
|
|
|
|
|
const float outgoingX = screenW * 0.68f;
|
|
|
|
|
|
|
|
|
|
|
|
int inIdx = 0, outIdx = 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
|
|
|
|
for (const auto& entry : entries) {
|
|
|
|
|
|
float alpha = 1.0f - (entry.age / game::CombatTextEntry::LIFETIME);
|
|
|
|
|
|
float yOffset = 200.0f - entry.age * 60.0f;
|
2026-02-17 15:24:39 -08:00
|
|
|
|
const bool outgoing = entry.isPlayerSource;
|
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
|
|
|
|
|
|
|
|
|
|
ImVec4 color;
|
|
|
|
|
|
char text[64];
|
|
|
|
|
|
switch (entry.type) {
|
|
|
|
|
|
case game::CombatTextEntry::MELEE_DAMAGE:
|
|
|
|
|
|
case game::CombatTextEntry::SPELL_DAMAGE:
|
|
|
|
|
|
snprintf(text, sizeof(text), "-%d", entry.amount);
|
2026-02-17 15:24:39 -08:00
|
|
|
|
color = outgoing ?
|
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
|
|
|
|
ImVec4(1.0f, 1.0f, 0.3f, alpha) : // Outgoing = yellow
|
|
|
|
|
|
ImVec4(1.0f, 0.3f, 0.3f, alpha); // Incoming = red
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::CRIT_DAMAGE:
|
|
|
|
|
|
snprintf(text, sizeof(text), "-%d!", entry.amount);
|
2026-02-17 15:24:39 -08:00
|
|
|
|
color = outgoing ?
|
|
|
|
|
|
ImVec4(1.0f, 0.8f, 0.0f, alpha) : // Outgoing crit = bright yellow
|
|
|
|
|
|
ImVec4(1.0f, 0.5f, 0.0f, alpha); // Incoming crit = orange
|
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
|
|
|
|
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:
|
2026-02-17 15:24:39 -08:00
|
|
|
|
// outgoing=true: enemy dodged player's attack
|
|
|
|
|
|
// outgoing=false: player dodged incoming attack
|
|
|
|
|
|
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);
|
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
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::PARRY:
|
2026-02-17 15:24:39 -08:00
|
|
|
|
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);
|
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
|
|
|
|
break;
|
2026-03-09 16:55:23 -07:00
|
|
|
|
case game::CombatTextEntry::BLOCK:
|
2026-03-11 03:36:45 -07:00
|
|
|
|
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");
|
2026-03-09 16:55:23 -07:00
|
|
|
|
color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha)
|
|
|
|
|
|
: ImVec4(0.4f, 0.9f, 1.0f, alpha);
|
|
|
|
|
|
break;
|
2026-03-13 23:32:57 -07:00
|
|
|
|
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;
|
2026-03-09 16:55:23 -07:00
|
|
|
|
case game::CombatTextEntry::PERIODIC_DAMAGE:
|
|
|
|
|
|
snprintf(text, sizeof(text), "-%d", entry.amount);
|
|
|
|
|
|
color = outgoing ?
|
|
|
|
|
|
ImVec4(1.0f, 0.9f, 0.3f, alpha) : // Outgoing DoT = pale yellow
|
|
|
|
|
|
ImVec4(1.0f, 0.4f, 0.4f, alpha); // Incoming DoT = pale red
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::PERIODIC_HEAL:
|
|
|
|
|
|
snprintf(text, sizeof(text), "+%d", entry.amount);
|
|
|
|
|
|
color = ImVec4(0.4f, 1.0f, 0.5f, alpha);
|
|
|
|
|
|
break;
|
2026-03-17 10:54:07 -07:00
|
|
|
|
case game::CombatTextEntry::ENVIRONMENTAL: {
|
|
|
|
|
|
const char* envLabel = "";
|
|
|
|
|
|
switch (entry.powerType) {
|
|
|
|
|
|
case 0: envLabel = "Fatigue "; break;
|
|
|
|
|
|
case 1: envLabel = "Drowning "; break;
|
|
|
|
|
|
case 2: envLabel = ""; break; // Fall: just show the number (WoW convention)
|
|
|
|
|
|
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);
|
2026-03-09 16:55:23 -07:00
|
|
|
|
color = ImVec4(0.9f, 0.5f, 0.2f, alpha); // Orange for environmental
|
|
|
|
|
|
break;
|
2026-03-17 10:54:07 -07:00
|
|
|
|
}
|
2026-03-09 16:55:23 -07:00
|
|
|
|
case game::CombatTextEntry::ENERGIZE:
|
|
|
|
|
|
snprintf(text, sizeof(text), "+%d", entry.amount);
|
2026-03-13 06:08:21 -07:00
|
|
|
|
switch (entry.powerType) {
|
|
|
|
|
|
case 1: color = ImVec4(1.0f, 0.2f, 0.2f, alpha); break; // Rage: red
|
|
|
|
|
|
case 2: color = ImVec4(1.0f, 0.6f, 0.1f, alpha); break; // Focus: orange
|
|
|
|
|
|
case 3: color = ImVec4(1.0f, 0.9f, 0.2f, alpha); break; // Energy: yellow
|
|
|
|
|
|
case 6: color = ImVec4(0.3f, 0.9f, 0.8f, alpha); break; // Runic Power: teal
|
|
|
|
|
|
default: color = ImVec4(0.3f, 0.6f, 1.0f, alpha); break; // Mana (0): blue
|
|
|
|
|
|
}
|
2026-03-09 16:55:23 -07:00
|
|
|
|
break;
|
2026-03-13 23:56:44 -07:00
|
|
|
|
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;
|
2026-03-09 17:13:31 -07:00
|
|
|
|
case game::CombatTextEntry::XP_GAIN:
|
|
|
|
|
|
snprintf(text, sizeof(text), "+%d XP", entry.amount);
|
|
|
|
|
|
color = ImVec4(0.7f, 0.3f, 1.0f, alpha); // Purple for XP
|
|
|
|
|
|
break;
|
2026-03-09 20:15:34 -07:00
|
|
|
|
case game::CombatTextEntry::IMMUNE:
|
|
|
|
|
|
snprintf(text, sizeof(text), "Immune!");
|
|
|
|
|
|
color = ImVec4(0.9f, 0.9f, 0.9f, alpha); // White for immune
|
|
|
|
|
|
break;
|
2026-03-11 03:23:01 -07:00
|
|
|
|
case game::CombatTextEntry::ABSORB:
|
2026-03-11 03:28:19 -07:00
|
|
|
|
if (entry.amount > 0)
|
|
|
|
|
|
snprintf(text, sizeof(text), "Absorbed %d", entry.amount);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(text, sizeof(text), "Absorbed");
|
2026-03-11 03:23:01 -07:00
|
|
|
|
color = ImVec4(0.5f, 0.8f, 1.0f, alpha); // Light blue for absorb
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::RESIST:
|
2026-03-11 03:28:19 -07:00
|
|
|
|
if (entry.amount > 0)
|
|
|
|
|
|
snprintf(text, sizeof(text), "Resisted %d", entry.amount);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(text, sizeof(text), "Resisted");
|
2026-03-11 03:23:01 -07:00
|
|
|
|
color = ImVec4(0.7f, 0.7f, 0.7f, alpha); // Grey for resist
|
|
|
|
|
|
break;
|
2026-03-13 23:08:49 -07:00
|
|
|
|
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;
|
2026-03-17 14:26:10 -07:00
|
|
|
|
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");
|
2026-03-13 23:08:49 -07:00
|
|
|
|
color = outgoing ? ImVec4(0.85f, 0.75f, 1.0f, alpha)
|
|
|
|
|
|
: ImVec4(0.75f, 0.85f, 1.0f, alpha);
|
|
|
|
|
|
break;
|
2026-03-17 14:26:10 -07:00
|
|
|
|
}
|
2026-03-12 09:41:45 -07:00
|
|
|
|
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!");
|
2026-03-12 06:06:41 -07:00
|
|
|
|
color = ImVec4(1.0f, 0.85f, 0.0f, alpha); // Gold for proc
|
|
|
|
|
|
break;
|
2026-03-12 09:41:45 -07:00
|
|
|
|
}
|
2026-03-13 19:58:37 -07:00
|
|
|
|
case game::CombatTextEntry::DISPEL:
|
2026-03-13 23:00:49 -07:00
|
|
|
|
if (entry.spellId != 0) {
|
|
|
|
|
|
const std::string& dispelledName = gameHandler.getSpellName(entry.spellId);
|
|
|
|
|
|
if (!dispelledName.empty())
|
|
|
|
|
|
snprintf(text, sizeof(text), "Dispel %s", dispelledName.c_str());
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(text, sizeof(text), "Dispel");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
snprintf(text, sizeof(text), "Dispel");
|
|
|
|
|
|
}
|
2026-03-13 19:58:37 -07:00
|
|
|
|
color = ImVec4(0.6f, 0.9f, 1.0f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::STEAL:
|
2026-03-13 23:00:49 -07:00
|
|
|
|
if (entry.spellId != 0) {
|
|
|
|
|
|
const std::string& stolenName = gameHandler.getSpellName(entry.spellId);
|
|
|
|
|
|
if (!stolenName.empty())
|
|
|
|
|
|
snprintf(text, sizeof(text), "Spellsteal %s", stolenName.c_str());
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(text, sizeof(text), "Spellsteal");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
snprintf(text, sizeof(text), "Spellsteal");
|
|
|
|
|
|
}
|
2026-03-13 19:58:37 -07:00
|
|
|
|
color = ImVec4(0.8f, 0.7f, 1.0f, alpha);
|
|
|
|
|
|
break;
|
2026-03-13 20:45:26 -07:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-03-13 22:22:00 -07:00
|
|
|
|
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;
|
2026-03-17 14:38:57 -07:00
|
|
|
|
case game::CombatTextEntry::HONOR_GAIN:
|
|
|
|
|
|
snprintf(text, sizeof(text), "+%d Honor", entry.amount);
|
|
|
|
|
|
color = ImVec4(1.0f, 0.85f, 0.0f, alpha); // Gold for honor
|
|
|
|
|
|
break;
|
2026-03-17 18:51:48 -07:00
|
|
|
|
case game::CombatTextEntry::GLANCING:
|
|
|
|
|
|
snprintf(text, sizeof(text), "~%d", entry.amount);
|
|
|
|
|
|
color = outgoing ?
|
|
|
|
|
|
ImVec4(0.75f, 0.75f, 0.5f, alpha) : // Outgoing glancing = muted yellow
|
|
|
|
|
|
ImVec4(0.75f, 0.35f, 0.35f, alpha); // Incoming glancing = muted red
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::CRUSHING:
|
|
|
|
|
|
snprintf(text, sizeof(text), "%d!", entry.amount);
|
|
|
|
|
|
color = outgoing ?
|
|
|
|
|
|
ImVec4(1.0f, 0.55f, 0.1f, alpha) : // Outgoing crushing = orange
|
|
|
|
|
|
ImVec4(1.0f, 0.15f, 0.15f, alpha); // Incoming crushing = bright red
|
|
|
|
|
|
break;
|
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
|
|
|
|
default:
|
|
|
|
|
|
snprintf(text, sizeof(text), "%d", entry.amount);
|
|
|
|
|
|
color = ImVec4(1.0f, 1.0f, 1.0f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-17 15:24:39 -08:00
|
|
|
|
// Outgoing → right side (near target), incoming → center-left (near player)
|
|
|
|
|
|
int& idx = outgoing ? outIdx : inIdx;
|
|
|
|
|
|
float baseX = outgoing ? outgoingX : incomingX;
|
|
|
|
|
|
float xOffset = baseX + (idx % 3 - 1) * 60.0f;
|
|
|
|
|
|
++idx;
|
2026-03-12 08:00:27 -07:00
|
|
|
|
|
|
|
|
|
|
// Crits render at 1.35× normal font size for visual impact
|
|
|
|
|
|
bool isCrit = (entry.type == game::CombatTextEntry::CRIT_DAMAGE ||
|
|
|
|
|
|
entry.type == game::CombatTextEntry::CRIT_HEAL);
|
|
|
|
|
|
ImFont* font = ImGui::GetFont();
|
|
|
|
|
|
float baseFontSize = ImGui::GetFontSize();
|
|
|
|
|
|
float renderFontSize = isCrit ? baseFontSize * 1.35f : baseFontSize;
|
|
|
|
|
|
|
|
|
|
|
|
// Advance cursor so layout accounting is correct, then read screen pos
|
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();
|
|
|
|
|
|
|
|
|
|
|
|
// Drop shadow for readability over complex backgrounds
|
|
|
|
|
|
ImU32 shadowCol = IM_COL32(0, 0, 0, static_cast<int>(alpha * 180));
|
|
|
|
|
|
ImU32 textCol = ImGui::ColorConvertFloat4ToU32(color);
|
|
|
|
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
|
|
|
|
dl->AddText(font, renderFontSize, ImVec2(screenPos.x + 1.0f, screenPos.y + 1.0f),
|
|
|
|
|
|
shadowCol, text);
|
|
|
|
|
|
dl->AddText(font, renderFontSize, screenPos, textCol, text);
|
|
|
|
|
|
|
|
|
|
|
|
// Reserve space so ImGui doesn't clip the window prematurely
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 04:04:27 -07:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// DPS / HPS Meter
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderDPSMeter(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!showDPSMeter_) return;
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
auto* unit = dynamic_cast<game::Unit*>(entityPtr.get());
|
|
|
|
|
|
if (!unit || unit->getMaxHealth() == 0) continue;
|
|
|
|
|
|
|
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-10 07:25:04 -07:00
|
|
|
|
// Player nameplates are always shown; NPC nameplates respect the V-key toggle
|
|
|
|
|
|
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-09 17:01:38 -07:00
|
|
|
|
float dist = glm::length(renderPos - camPos);
|
2026-03-10 07:25:04 -07:00
|
|
|
|
float cullDist = (isTarget || isPlayer) ? 40.0f : 20.0f;
|
2026-03-10 05:38:52 -07:00
|
|
|
|
if (dist > 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
|
|
|
|
|
|
float alpha = dist < (cullDist - 5.0f) ? 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-09 17:01:38 -07:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 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-11 22:49:54 -07:00
|
|
|
|
const float barW = 80.0f * nameplateScale_;
|
|
|
|
|
|
const float barH = 8.0f * nameplateScale_;
|
2026-03-09 17:01:38 -07:00
|
|
|
|
const float barX = sx - barW * 0.5f;
|
|
|
|
|
|
|
|
|
|
|
|
float healthPct = std::clamp(
|
|
|
|
|
|
static_cast<float>(unit->getHealth()) / static_cast<float>(unit->getMaxHealth()),
|
|
|
|
|
|
0.0f, 1.0f);
|
|
|
|
|
|
|
|
|
|
|
|
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-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);
|
|
|
|
|
|
const float cbH = 6.0f * nameplateScale_;
|
|
|
|
|
|
|
|
|
|
|
|
// Spell name above the cast bar
|
|
|
|
|
|
const std::string& spellName = gameHandler.getSpellName(cs->spellId);
|
|
|
|
|
|
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-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();
|
|
|
|
|
|
const float dotSize = 6.0f * nameplateScale_;
|
|
|
|
|
|
const float dotGap = 2.0f;
|
|
|
|
|
|
float dotX = barX;
|
|
|
|
|
|
for (const auto& aura : auras) {
|
|
|
|
|
|
if (aura.isEmpty() || aura.casterGuid != pguid) continue;
|
|
|
|
|
|
uint8_t dispelType = gameHandler.getSpellDispelType(aura.spellId);
|
|
|
|
|
|
ImU32 dotCol;
|
|
|
|
|
|
switch (dispelType) {
|
|
|
|
|
|
case 1: dotCol = IM_COL32( 64, 128, 255, A(210)); break; // Magic - blue
|
|
|
|
|
|
case 2: dotCol = IM_COL32(160, 32, 240, A(210)); break; // Curse - purple
|
|
|
|
|
|
case 3: dotCol = IM_COL32(180, 140, 40, A(210)); break; // Disease - yellow-brown
|
|
|
|
|
|
case 4: dotCol = IM_COL32( 50, 200, 50, A(210)); break; // Poison - green
|
|
|
|
|
|
default: dotCol = IM_COL32(170, 170, 170, A(170)); break; // Other - grey
|
|
|
|
|
|
}
|
|
|
|
|
|
drawList->AddRectFilled(ImVec2(dotX, nameplateBottom),
|
|
|
|
|
|
ImVec2(dotX + dotSize, nameplateBottom + dotSize), dotCol, 1.0f);
|
|
|
|
|
|
drawList->AddRect (ImVec2(dotX - 1.0f, nameplateBottom - 1.0f),
|
|
|
|
|
|
ImVec2(dotX + dotSize + 1.0f, nameplateBottom + dotSize + 1.0f),
|
|
|
|
|
|
IM_COL32(0, 0, 0, A(150)), 1.0f);
|
2026-03-12 13:36:06 -07:00
|
|
|
|
|
|
|
|
|
|
// Spell name tooltip on hover
|
|
|
|
|
|
{
|
|
|
|
|
|
ImVec2 mouse = ImGui::GetMousePos();
|
|
|
|
|
|
if (mouse.x >= dotX && mouse.x < dotX + dotSize &&
|
|
|
|
|
|
mouse.y >= nameplateBottom && mouse.y < nameplateBottom + dotSize) {
|
|
|
|
|
|
const std::string& dotSpellName = gameHandler.getSpellName(aura.spellId);
|
|
|
|
|
|
if (!dotSpellName.empty())
|
|
|
|
|
|
ImGui::SetTooltip("%s", dotSpellName.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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-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-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
|
|
|
|
|
|
{
|
|
|
|
|
|
static const struct { const char* sym; ImU32 col; } kNPMarks[] = {
|
|
|
|
|
|
{ "\xe2\x98\x85", IM_COL32(255,220, 50,230) }, // Star
|
|
|
|
|
|
{ "\xe2\x97\x8f", IM_COL32(255,140, 0,230) }, // Circle
|
|
|
|
|
|
{ "\xe2\x97\x86", IM_COL32(160, 32,240,230) }, // Diamond
|
|
|
|
|
|
{ "\xe2\x96\xb2", IM_COL32( 50,200, 50,230) }, // Triangle
|
|
|
|
|
|
{ "\xe2\x97\x8c", IM_COL32( 80,160,255,230) }, // Moon
|
|
|
|
|
|
{ "\xe2\x96\xa0", IM_COL32( 50,200,220,230) }, // Square
|
|
|
|
|
|
{ "\xe2\x9c\x9d", IM_COL32(255, 80, 80,230) }, // Cross
|
|
|
|
|
|
{ "\xe2\x98\xa0", IM_COL32(255,255,255,230) }, // Skull
|
|
|
|
|
|
};
|
|
|
|
|
|
uint8_t raidMark = gameHandler.getEntityRaidMark(guid);
|
|
|
|
|
|
if (raidMark < game::GameHandler::kRaidMarkCount) {
|
|
|
|
|
|
float markX = nameX - 14.0f;
|
|
|
|
|
|
drawList->AddText(ImVec2(markX + 1.0f, nameY + 1.0f), IM_COL32(0,0,0,120), kNPMarks[raidMark].sym);
|
|
|
|
|
|
drawList->AddText(ImVec2(markX, nameY), kNPMarks[raidMark].col, kNPMarks[raidMark].sym);
|
|
|
|
|
|
}
|
2026-03-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-12 00:26:47 -07:00
|
|
|
|
// Click to target / right-click context: detect clicks inside the nameplate region
|
|
|
|
|
|
if (!ImGui::GetIO().WantCaptureMouse) {
|
2026-03-10 05:50:26 -07:00
|
|
|
|
ImVec2 mouse = ImGui::GetIO().MousePos;
|
|
|
|
|
|
float nx0 = nameX - 2.0f;
|
|
|
|
|
|
float ny0 = nameY - 1.0f;
|
|
|
|
|
|
float nx1 = nameX + textSize.x + 2.0f;
|
|
|
|
|
|
float ny1 = sy + barH + 2.0f;
|
|
|
|
|
|
if (mouse.x >= nx0 && mouse.x <= nx1 && mouse.y >= ny0 && mouse.y <= ny1) {
|
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")) {
|
|
|
|
|
|
selectedChatType = 4;
|
|
|
|
|
|
strncpy(whisperTargetBuffer, ctxName.c_str(), sizeof(whisperTargetBuffer) - 1);
|
|
|
|
|
|
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
|
{
|
|
|
|
|
|
static const struct { const char* sym; ImU32 col; } kCellMarks[] = {
|
|
|
|
|
|
{ "\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)) {
|
|
|
|
|
|
static const char* kDispelNames[] = { "", "Magic", "Curse", "Disease", "Poison" };
|
|
|
|
|
|
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-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")) {
|
|
|
|
|
|
selectedChatType = 4;
|
|
|
|
|
|
strncpy(whisperTargetBuffer, m.name.c_str(), sizeof(whisperTargetBuffer) - 1);
|
|
|
|
|
|
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
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")) {
|
|
|
|
|
|
static const char* kRaidMarkNames[] = {
|
|
|
|
|
|
"{*} Star", "{O} Circle", "{<>} Diamond", "{^} Triangle",
|
|
|
|
|
|
"{)} Moon", "{ } Square", "{x} Cross", "{8} Skull"
|
|
|
|
|
|
};
|
|
|
|
|
|
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
|
|
|
|
|
|
? ImVec4(1.0f, 0.85f, 0.0f, 1.0f)
|
|
|
|
|
|
: ImVec4(0.85f, 0.85f, 0.85f, 1.0f);
|
|
|
|
|
|
{
|
|
|
|
|
|
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-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
|
|
|
|
|
|
{
|
|
|
|
|
|
static const struct { const char* sym; ImU32 col; } kPartyMarks[] = {
|
|
|
|
|
|
{ "\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)
|
|
|
|
|
|
: (pct > 0.5f ? ImVec4(0.2f, 0.8f, 0.2f, 1.0f) :
|
|
|
|
|
|
pct > 0.2f ? ImVec4(0.8f, 0.8f, 0.2f, 1.0f) :
|
|
|
|
|
|
ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
|
|
|
|
|
|
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",
|
|
|
|
|
|
(int)hp / 1000, (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) {
|
|
|
|
|
|
case 0: powerColor = ImVec4(0.2f, 0.2f, 0.9f, 1.0f); break; // Mana (blue)
|
|
|
|
|
|
case 1: powerColor = ImVec4(0.9f, 0.2f, 0.2f, 1.0f); break; // Rage (red)
|
2026-03-09 17:09:48 -07:00
|
|
|
|
case 2: powerColor = ImVec4(0.9f, 0.6f, 0.1f, 1.0f); break; // Focus (orange)
|
2026-02-26 10:25:55 -08:00
|
|
|
|
case 3: powerColor = ImVec4(0.9f, 0.9f, 0.2f, 1.0f); break; // Energy (yellow)
|
2026-03-09 17:09:48 -07:00
|
|
|
|
case 4: powerColor = ImVec4(0.5f, 0.9f, 0.3f, 1.0f); break; // Happiness (green)
|
|
|
|
|
|
case 6: powerColor = ImVec4(0.8f, 0.1f, 0.2f, 1.0f); break; // Runic Power (crimson)
|
|
|
|
|
|
case 7: powerColor = ImVec4(0.4f, 0.1f, 0.6f, 1.0f); break; // Soul Shards (purple)
|
2026-02-26 10:25:55 -08:00
|
|
|
|
default: powerColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f); break;
|
|
|
|
|
|
}
|
|
|
|
|
|
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()) {
|
|
|
|
|
|
static const char* kDispelNames[] = { "", "Magic", "Curse", "Disease", "Poison" };
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.8f, 0.8f, 0.2f, 1.0f));
|
|
|
|
|
|
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")) {
|
|
|
|
|
|
selectedChatType = 4; // WHISPER
|
|
|
|
|
|
strncpy(whisperTargetBuffer, member.name.c_str(), sizeof(whisperTargetBuffer) - 1);
|
|
|
|
|
|
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
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")) {
|
|
|
|
|
|
static const char* kRaidMarkNames[] = {
|
|
|
|
|
|
"{*} Star", "{O} Circle", "{<>} Diamond", "{^} Triangle",
|
|
|
|
|
|
"{)} Moon", "{ } Square", "{x} Cross", "{8} Skull"
|
|
|
|
|
|
};
|
|
|
|
|
|
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)
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.1f, 1.0f),
|
|
|
|
|
|
"\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 01:51:18 -07:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Reputation change toasts
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderRepToasts(float deltaTime) {
|
|
|
|
|
|
for (auto& e : repToasts_) e.age += deltaTime;
|
|
|
|
|
|
repToasts_.erase(
|
|
|
|
|
|
std::remove_if(repToasts_.begin(), repToasts_.end(),
|
|
|
|
|
|
[](const RepToastEntry& e) { return e.age >= kRepToastLifetime; }),
|
|
|
|
|
|
repToasts_.end());
|
|
|
|
|
|
|
|
|
|
|
|
if (repToasts_.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;
|
|
|
|
|
|
|
|
|
|
|
|
// Stack toasts in the lower-right corner (above the action bar), newest on top
|
|
|
|
|
|
const float toastW = 220.0f;
|
|
|
|
|
|
const float toastH = 26.0f;
|
|
|
|
|
|
const float padY = 4.0f;
|
|
|
|
|
|
const float rightEdge = screenW - 14.0f;
|
|
|
|
|
|
const float baseY = screenH - 180.0f;
|
|
|
|
|
|
|
|
|
|
|
|
const int count = static_cast<int>(repToasts_.size());
|
|
|
|
|
|
|
|
|
|
|
|
ImDrawList* draw = ImGui::GetForegroundDrawList();
|
|
|
|
|
|
ImFont* font = ImGui::GetFont();
|
|
|
|
|
|
float fontSize = ImGui::GetFontSize();
|
|
|
|
|
|
|
|
|
|
|
|
// Compute standing tier label (Exalted, Revered, Honored, Friendly, Neutral, Unfriendly, Hostile, Hated)
|
|
|
|
|
|
auto standingLabel = [](int32_t s) -> const char* {
|
|
|
|
|
|
if (s >= 42000) return "Exalted";
|
|
|
|
|
|
if (s >= 21000) return "Revered";
|
|
|
|
|
|
if (s >= 9000) return "Honored";
|
|
|
|
|
|
if (s >= 3000) return "Friendly";
|
|
|
|
|
|
if (s >= 0) return "Neutral";
|
|
|
|
|
|
if (s >= -3000) return "Unfriendly";
|
|
|
|
|
|
if (s >= -6000) return "Hostile";
|
|
|
|
|
|
return "Hated";
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < count; ++i) {
|
|
|
|
|
|
const auto& e = repToasts_[i];
|
|
|
|
|
|
// Slide in from right on appear, slide out at end
|
|
|
|
|
|
constexpr float kSlideDur = 0.3f;
|
|
|
|
|
|
float slideIn = std::min(e.age, kSlideDur) / kSlideDur;
|
|
|
|
|
|
float slideOut = std::min(std::max(0.0f, kRepToastLifetime - e.age), kSlideDur) / kSlideDur;
|
|
|
|
|
|
float slide = std::min(slideIn, slideOut);
|
|
|
|
|
|
|
|
|
|
|
|
float alpha = std::clamp(slide, 0.0f, 1.0f);
|
|
|
|
|
|
float xFull = rightEdge - toastW;
|
|
|
|
|
|
float xStart = screenW + 10.0f;
|
|
|
|
|
|
float toastX = xStart + (xFull - xStart) * slide;
|
|
|
|
|
|
float toastY = baseY - i * (toastH + padY);
|
|
|
|
|
|
|
|
|
|
|
|
ImVec2 tl(toastX, toastY);
|
|
|
|
|
|
ImVec2 br(toastX + toastW, toastY + toastH);
|
|
|
|
|
|
|
|
|
|
|
|
// Background
|
|
|
|
|
|
draw->AddRectFilled(tl, br, IM_COL32(15, 15, 20, (int)(alpha * 200)), 4.0f);
|
|
|
|
|
|
// Border: green for gain, red for loss
|
|
|
|
|
|
ImU32 borderCol = (e.delta > 0)
|
|
|
|
|
|
? IM_COL32(80, 200, 80, (int)(alpha * 220))
|
|
|
|
|
|
: IM_COL32(200, 60, 60, (int)(alpha * 220));
|
|
|
|
|
|
draw->AddRect(tl, br, borderCol, 4.0f, 0, 1.5f);
|
|
|
|
|
|
|
|
|
|
|
|
// Delta text: "+250" or "-250"
|
|
|
|
|
|
char deltaBuf[16];
|
|
|
|
|
|
snprintf(deltaBuf, sizeof(deltaBuf), "%+d", e.delta);
|
|
|
|
|
|
ImU32 deltaCol = (e.delta > 0) ? IM_COL32(80, 220, 80, (int)(alpha * 255))
|
|
|
|
|
|
: IM_COL32(220, 70, 70, (int)(alpha * 255));
|
|
|
|
|
|
draw->AddText(font, fontSize, ImVec2(tl.x + 6.0f, tl.y + (toastH - fontSize) * 0.5f),
|
|
|
|
|
|
deltaCol, deltaBuf);
|
|
|
|
|
|
|
|
|
|
|
|
// Faction name + standing
|
|
|
|
|
|
char nameBuf[64];
|
|
|
|
|
|
snprintf(nameBuf, sizeof(nameBuf), "%s (%s)", e.factionName.c_str(), standingLabel(e.standing));
|
|
|
|
|
|
draw->AddText(font, fontSize * 0.85f, ImVec2(tl.x + 44.0f, tl.y + (toastH - fontSize * 0.85f) * 0.5f),
|
|
|
|
|
|
IM_COL32(210, 210, 210, (int)(alpha * 220)), nameBuf);
|
2026-03-12 04:53:03 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderQuestCompleteToasts(float deltaTime) {
|
|
|
|
|
|
for (auto& e : questCompleteToasts_) e.age += deltaTime;
|
|
|
|
|
|
questCompleteToasts_.erase(
|
|
|
|
|
|
std::remove_if(questCompleteToasts_.begin(), questCompleteToasts_.end(),
|
|
|
|
|
|
[](const QuestCompleteToastEntry& e) { return e.age >= kQuestCompleteToastLifetime; }),
|
|
|
|
|
|
questCompleteToasts_.end());
|
|
|
|
|
|
|
|
|
|
|
|
if (questCompleteToasts_.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;
|
|
|
|
|
|
|
|
|
|
|
|
const float toastW = 260.0f;
|
|
|
|
|
|
const float toastH = 40.0f;
|
|
|
|
|
|
const float padY = 4.0f;
|
|
|
|
|
|
const float baseY = screenH - 220.0f; // above rep toasts
|
|
|
|
|
|
|
|
|
|
|
|
ImDrawList* draw = ImGui::GetForegroundDrawList();
|
|
|
|
|
|
ImFont* font = ImGui::GetFont();
|
|
|
|
|
|
float fontSize = ImGui::GetFontSize();
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < static_cast<int>(questCompleteToasts_.size()); ++i) {
|
|
|
|
|
|
const auto& e = questCompleteToasts_[i];
|
|
|
|
|
|
constexpr float kSlideDur = 0.3f;
|
|
|
|
|
|
float slideIn = std::min(e.age, kSlideDur) / kSlideDur;
|
|
|
|
|
|
float slideOut = std::min(std::max(0.0f, kQuestCompleteToastLifetime - e.age), kSlideDur) / kSlideDur;
|
|
|
|
|
|
float slide = std::min(slideIn, slideOut);
|
|
|
|
|
|
float alpha = std::clamp(slide, 0.0f, 1.0f);
|
|
|
|
|
|
|
|
|
|
|
|
float xFull = screenW - 14.0f - toastW;
|
|
|
|
|
|
float xStart = screenW + 10.0f;
|
|
|
|
|
|
float toastX = xStart + (xFull - xStart) * slide;
|
|
|
|
|
|
float toastY = baseY - i * (toastH + padY);
|
|
|
|
|
|
|
|
|
|
|
|
ImVec2 tl(toastX, toastY);
|
|
|
|
|
|
ImVec2 br(toastX + toastW, toastY + toastH);
|
|
|
|
|
|
|
|
|
|
|
|
// Background + gold border (quest completion)
|
|
|
|
|
|
draw->AddRectFilled(tl, br, IM_COL32(20, 18, 8, (int)(alpha * 210)), 5.0f);
|
|
|
|
|
|
draw->AddRect(tl, br, IM_COL32(220, 180, 30, (int)(alpha * 230)), 5.0f, 0, 1.5f);
|
|
|
|
|
|
|
|
|
|
|
|
// Scroll icon placeholder (gold diamond)
|
|
|
|
|
|
float iconCx = tl.x + 18.0f;
|
|
|
|
|
|
float iconCy = tl.y + toastH * 0.5f;
|
|
|
|
|
|
draw->AddCircleFilled(ImVec2(iconCx, iconCy), 7.0f, IM_COL32(210, 170, 20, (int)(alpha * 230)));
|
|
|
|
|
|
draw->AddCircle (ImVec2(iconCx, iconCy), 7.0f, IM_COL32(255, 220, 50, (int)(alpha * 200)));
|
|
|
|
|
|
|
|
|
|
|
|
// "Quest Complete" header in gold
|
|
|
|
|
|
const char* header = "Quest Complete";
|
|
|
|
|
|
draw->AddText(font, fontSize * 0.78f,
|
|
|
|
|
|
ImVec2(tl.x + 34.0f, tl.y + 4.0f),
|
|
|
|
|
|
IM_COL32(240, 200, 40, (int)(alpha * 240)), header);
|
|
|
|
|
|
|
|
|
|
|
|
// Quest title in off-white
|
|
|
|
|
|
const char* titleStr = e.title.empty() ? "Unknown Quest" : e.title.c_str();
|
|
|
|
|
|
draw->AddText(font, fontSize * 0.82f,
|
|
|
|
|
|
ImVec2(tl.x + 34.0f, tl.y + toastH * 0.5f + 1.0f),
|
|
|
|
|
|
IM_COL32(220, 215, 195, (int)(alpha * 220)), titleStr);
|
2026-03-12 01:51:18 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 05:44:25 -07:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Zone Entry Toast
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderZoneToasts(float deltaTime) {
|
|
|
|
|
|
for (auto& e : zoneToasts_) e.age += deltaTime;
|
|
|
|
|
|
zoneToasts_.erase(
|
|
|
|
|
|
std::remove_if(zoneToasts_.begin(), zoneToasts_.end(),
|
|
|
|
|
|
[](const ZoneToastEntry& e) { return e.age >= kZoneToastLifetime; }),
|
|
|
|
|
|
zoneToasts_.end());
|
|
|
|
|
|
|
|
|
|
|
|
if (zoneToasts_.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImDrawList* draw = ImGui::GetForegroundDrawList();
|
|
|
|
|
|
ImFont* font = ImGui::GetFont();
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < static_cast<int>(zoneToasts_.size()); ++i) {
|
|
|
|
|
|
const auto& e = zoneToasts_[i];
|
|
|
|
|
|
constexpr float kSlideDur = 0.35f;
|
|
|
|
|
|
float slideIn = std::min(e.age, kSlideDur) / kSlideDur;
|
|
|
|
|
|
float slideOut = std::min(std::max(0.0f, kZoneToastLifetime - e.age), kSlideDur) / kSlideDur;
|
|
|
|
|
|
float slide = std::min(slideIn, slideOut);
|
|
|
|
|
|
float alpha = std::clamp(slide, 0.0f, 1.0f);
|
|
|
|
|
|
|
|
|
|
|
|
// Measure text to size the toast
|
|
|
|
|
|
ImVec2 nameSz = font->CalcTextSizeA(14.0f, FLT_MAX, 0.0f, e.zoneName.c_str());
|
|
|
|
|
|
const char* header = "Entering:";
|
|
|
|
|
|
ImVec2 hdrSz = font->CalcTextSizeA(11.0f, FLT_MAX, 0.0f, header);
|
|
|
|
|
|
|
|
|
|
|
|
float toastW = std::max(nameSz.x, hdrSz.x) + 28.0f;
|
|
|
|
|
|
float toastH = 42.0f;
|
|
|
|
|
|
|
|
|
|
|
|
// Center the toast horizontally, appear just below the zone name area (top-center)
|
|
|
|
|
|
float toastX = (screenW - toastW) * 0.5f;
|
|
|
|
|
|
float toastY = 56.0f + i * (toastH + 4.0f);
|
|
|
|
|
|
// Slide down from above
|
|
|
|
|
|
float offY = (1.0f - slide) * (-toastH - 10.0f);
|
|
|
|
|
|
toastY += offY;
|
|
|
|
|
|
|
|
|
|
|
|
ImVec2 tl(toastX, toastY);
|
|
|
|
|
|
ImVec2 br(toastX + toastW, toastY + toastH);
|
|
|
|
|
|
|
|
|
|
|
|
draw->AddRectFilled(tl, br, IM_COL32(10, 10, 16, (int)(alpha * 200)), 6.0f);
|
|
|
|
|
|
draw->AddRect(tl, br, IM_COL32(160, 140, 80, (int)(alpha * 220)), 6.0f, 0, 1.2f);
|
|
|
|
|
|
|
|
|
|
|
|
float cx = tl.x + toastW * 0.5f;
|
|
|
|
|
|
draw->AddText(font, 11.0f,
|
|
|
|
|
|
ImVec2(cx - hdrSz.x * 0.5f, tl.y + 5.0f),
|
|
|
|
|
|
IM_COL32(180, 170, 120, (int)(alpha * 200)), header);
|
|
|
|
|
|
draw->AddText(font, 14.0f,
|
|
|
|
|
|
ImVec2(cx - nameSz.x * 0.5f, tl.y + toastH * 0.5f + 1.0f),
|
|
|
|
|
|
IM_COL32(255, 230, 140, (int)(alpha * 240)), e.zoneName.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 11:06:40 -07:00
|
|
|
|
// ─── Area Trigger Message Toasts ─────────────────────────────────────────────
|
|
|
|
|
|
void GameScreen::renderAreaTriggerToasts(float deltaTime, game::GameHandler& gameHandler) {
|
|
|
|
|
|
// Drain any pending messages from GameHandler
|
|
|
|
|
|
while (gameHandler.hasAreaTriggerMsg()) {
|
|
|
|
|
|
AreaTriggerToast t;
|
|
|
|
|
|
t.text = gameHandler.popAreaTriggerMsg();
|
|
|
|
|
|
t.age = 0.0f;
|
|
|
|
|
|
areaTriggerToasts_.push_back(std::move(t));
|
|
|
|
|
|
if (areaTriggerToasts_.size() > 4)
|
|
|
|
|
|
areaTriggerToasts_.erase(areaTriggerToasts_.begin());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Age and prune
|
|
|
|
|
|
constexpr float kLifetime = 4.5f;
|
|
|
|
|
|
for (auto& t : areaTriggerToasts_) t.age += deltaTime;
|
|
|
|
|
|
areaTriggerToasts_.erase(
|
|
|
|
|
|
std::remove_if(areaTriggerToasts_.begin(), areaTriggerToasts_.end(),
|
|
|
|
|
|
[](const AreaTriggerToast& t) { return t.age >= kLifetime; }),
|
|
|
|
|
|
areaTriggerToasts_.end());
|
|
|
|
|
|
if (areaTriggerToasts_.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;
|
|
|
|
|
|
|
|
|
|
|
|
ImDrawList* draw = ImGui::GetForegroundDrawList();
|
|
|
|
|
|
ImFont* font = ImGui::GetFont();
|
|
|
|
|
|
constexpr float kSlideDur = 0.35f;
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < static_cast<int>(areaTriggerToasts_.size()); ++i) {
|
|
|
|
|
|
const auto& t = areaTriggerToasts_[i];
|
|
|
|
|
|
|
|
|
|
|
|
float slideIn = std::min(t.age, kSlideDur) / kSlideDur;
|
|
|
|
|
|
float slideOut = std::min(std::max(0.0f, kLifetime - t.age), kSlideDur) / kSlideDur;
|
|
|
|
|
|
float alpha = std::clamp(std::min(slideIn, slideOut), 0.0f, 1.0f);
|
|
|
|
|
|
|
|
|
|
|
|
// Measure text
|
|
|
|
|
|
ImVec2 txtSz = font->CalcTextSizeA(13.0f, FLT_MAX, 0.0f, t.text.c_str());
|
|
|
|
|
|
float toastW = txtSz.x + 30.0f;
|
|
|
|
|
|
float toastH = 30.0f;
|
|
|
|
|
|
|
|
|
|
|
|
// Center horizontally, place below zone text (center of lower-third)
|
|
|
|
|
|
float toastX = (screenW - toastW) * 0.5f;
|
|
|
|
|
|
float toastY = screenH * 0.62f + i * (toastH + 3.0f);
|
|
|
|
|
|
// Slide up from below
|
|
|
|
|
|
float offY = (1.0f - std::min(slideIn, slideOut)) * (toastH + 12.0f);
|
|
|
|
|
|
toastY += offY;
|
|
|
|
|
|
|
|
|
|
|
|
ImVec2 tl(toastX, toastY);
|
|
|
|
|
|
ImVec2 br(toastX + toastW, toastY + toastH);
|
|
|
|
|
|
|
|
|
|
|
|
draw->AddRectFilled(tl, br, IM_COL32(8, 12, 22, (int)(alpha * 190)), 5.0f);
|
|
|
|
|
|
draw->AddRect(tl, br, IM_COL32(100, 160, 220, (int)(alpha * 200)), 5.0f, 0, 1.0f);
|
|
|
|
|
|
|
|
|
|
|
|
float cx = tl.x + toastW * 0.5f;
|
|
|
|
|
|
// Shadow
|
|
|
|
|
|
draw->AddText(font, 13.0f,
|
|
|
|
|
|
ImVec2(cx - txtSz.x * 0.5f + 1, tl.y + (toastH - txtSz.y) * 0.5f + 1),
|
|
|
|
|
|
IM_COL32(0, 0, 0, (int)(alpha * 180)), t.text.c_str());
|
|
|
|
|
|
// Text in light blue
|
|
|
|
|
|
draw->AddText(font, 13.0f,
|
|
|
|
|
|
ImVec2(cx - txtSz.x * 0.5f, tl.y + (toastH - txtSz.y) * 0.5f),
|
|
|
|
|
|
IM_COL32(180, 220, 255, (int)(alpha * 240)), t.text.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-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,
|
|
|
|
|
|
pct > 0.5f ? ImVec4(0.8f, 0.2f, 0.2f, 1.0f) :
|
|
|
|
|
|
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
|
|
|
|
|
|
case 1: bpColor = ImVec4(0.9f, 0.2f, 0.2f, 1.0f); break; // Rage: red
|
|
|
|
|
|
case 2: bpColor = ImVec4(0.9f, 0.6f, 0.1f, 1.0f); break; // Focus: orange
|
|
|
|
|
|
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
|
|
|
|
|
|
? ImVec4(0.2f, 0.75f, 0.2f, 1.0f)
|
|
|
|
|
|
: 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];
|
|
|
|
|
|
int s = (baRemain + 999) / 1000;
|
|
|
|
|
|
if (s >= 3600) snprintf(ts, sizeof(ts), "%dh", s / 3600);
|
|
|
|
|
|
else if (s >= 60) snprintf(ts, sizeof(ts), "%d:%02d", s / 60, s % 60);
|
|
|
|
|
|
else snprintf(ts, sizeof(ts), "%d", s);
|
|
|
|
|
|
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");
|
|
|
|
|
|
if (baRemain > 0) {
|
|
|
|
|
|
int s = baRemain / 1000;
|
|
|
|
|
|
char db[32];
|
|
|
|
|
|
if (s < 60) snprintf(db, sizeof(db), "Remaining: %ds", s);
|
|
|
|
|
|
else snprintf(db, sizeof(db), "Remaining: %dm %ds", s / 60, s % 60);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", db);
|
|
|
|
|
|
}
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
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
|
|
|
|
// Group Invite Popup (Phase 4)
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderGroupInvitePopup(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.hasPendingGroupInvite()) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 200), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("Group Invite", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) {
|
|
|
|
|
|
ImGui::Text("%s has invited you to a group.", gameHandler.getPendingInviterName().c_str());
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Button("Accept", ImVec2(130, 30))) {
|
|
|
|
|
|
gameHandler.acceptGroupInvite();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Decline", ImVec2(130, 30))) {
|
|
|
|
|
|
gameHandler.declineGroupInvite();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 13:58:02 -07:00
|
|
|
|
void GameScreen::renderDuelRequestPopup(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.hasPendingDuelRequest()) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 250), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("Duel Request", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) {
|
|
|
|
|
|
ImGui::Text("%s challenges you to a duel!", gameHandler.getDuelChallengerName().c_str());
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Button("Accept", ImVec2(130, 30))) {
|
|
|
|
|
|
gameHandler.acceptDuel();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Decline", ImVec2(130, 30))) {
|
|
|
|
|
|
gameHandler.forfeitDuel();
|
|
|
|
|
|
}
|
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
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 05:06:14 -07:00
|
|
|
|
void GameScreen::renderDuelCountdown(game::GameHandler& gameHandler) {
|
|
|
|
|
|
float remaining = gameHandler.getDuelCountdownRemaining();
|
|
|
|
|
|
if (remaining <= 0.0f) return;
|
|
|
|
|
|
|
|
|
|
|
|
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* dl = ImGui::GetForegroundDrawList();
|
|
|
|
|
|
ImFont* font = ImGui::GetFont();
|
|
|
|
|
|
float fontSize = ImGui::GetFontSize();
|
|
|
|
|
|
|
|
|
|
|
|
// Show integer countdown or "Fight!" when under 0.5s
|
|
|
|
|
|
char buf[32];
|
|
|
|
|
|
if (remaining > 0.5f) {
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "%d", static_cast<int>(std::ceil(remaining)));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "Fight!");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Large font by scaling — use 4x font size for dramatic effect
|
|
|
|
|
|
float scale = 4.0f;
|
|
|
|
|
|
float scaledSize = fontSize * scale;
|
|
|
|
|
|
ImVec2 textSz = font->CalcTextSizeA(scaledSize, FLT_MAX, 0.0f, buf);
|
|
|
|
|
|
float tx = (screenW - textSz.x) * 0.5f;
|
|
|
|
|
|
float ty = screenH * 0.35f - textSz.y * 0.5f;
|
|
|
|
|
|
|
|
|
|
|
|
// Pulsing alpha: fades in and out per second
|
|
|
|
|
|
float pulse = 0.75f + 0.25f * std::sin(static_cast<float>(ImGui::GetTime()) * 6.28f);
|
|
|
|
|
|
uint8_t alpha = static_cast<uint8_t>(255 * pulse);
|
|
|
|
|
|
|
|
|
|
|
|
// Color: golden countdown, red "Fight!"
|
|
|
|
|
|
ImU32 color = (remaining > 0.5f)
|
|
|
|
|
|
? IM_COL32(255, 200, 50, alpha)
|
|
|
|
|
|
: IM_COL32(255, 60, 60, alpha);
|
|
|
|
|
|
|
|
|
|
|
|
// Drop shadow
|
|
|
|
|
|
dl->AddText(font, scaledSize, ImVec2(tx + 2.0f, ty + 2.0f), IM_COL32(0, 0, 0, alpha / 2), buf);
|
|
|
|
|
|
dl->AddText(font, scaledSize, ImVec2(tx, ty), color, buf);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 14:15:59 -07:00
|
|
|
|
void GameScreen::renderItemTextWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isItemTextOpen()) 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 - 200, screenH * 0.15f),
|
|
|
|
|
|
ImGuiCond_FirstUseEver);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(400, 300), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
|
if (!ImGui::Begin("Book", &open, ImGuiWindowFlags_NoCollapse)) {
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
if (!open) gameHandler.closeItemText();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
gameHandler.closeItemText();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Parchment-toned background text
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.2f, 0.1f, 0.0f, 1.0f));
|
|
|
|
|
|
ImGui::TextWrapped("%s", gameHandler.getItemText().c_str());
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
if (ImGui::Button("Close", ImVec2(80, 0))) {
|
|
|
|
|
|
gameHandler.closeItemText();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 14:14:15 -07:00
|
|
|
|
void GameScreen::renderSharedQuestPopup(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.hasPendingSharedQuest()) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 175, 490), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(350, 0), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("Shared Quest", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) {
|
|
|
|
|
|
ImGui::Text("%s has shared a quest with you:", gameHandler.getSharedQuestSharerName().c_str());
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "\"%s\"", gameHandler.getSharedQuestTitle().c_str());
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Button("Accept", ImVec2(130, 30))) {
|
|
|
|
|
|
gameHandler.acceptSharedQuest();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Decline", ImVec2(130, 30))) {
|
|
|
|
|
|
gameHandler.declineSharedQuest();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 14:07:50 -07:00
|
|
|
|
void GameScreen::renderSummonRequestPopup(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.hasPendingSummonRequest()) return;
|
|
|
|
|
|
|
2026-03-09 14:08:49 -07:00
|
|
|
|
// Tick the timeout down
|
|
|
|
|
|
float dt = ImGui::GetIO().DeltaTime;
|
|
|
|
|
|
gameHandler.tickSummonTimeout(dt);
|
|
|
|
|
|
if (!gameHandler.hasPendingSummonRequest()) return; // expired
|
|
|
|
|
|
|
2026-03-09 14:07:50 -07:00
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 175, 430), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(350, 0), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("Summon Request", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) {
|
|
|
|
|
|
ImGui::Text("%s is summoning you.", gameHandler.getSummonerName().c_str());
|
|
|
|
|
|
float t = gameHandler.getSummonTimeoutSec();
|
|
|
|
|
|
if (t > 0.0f) {
|
|
|
|
|
|
ImGui::Text("Time remaining: %.0fs", t);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Button("Accept", ImVec2(130, 30))) {
|
|
|
|
|
|
gameHandler.acceptSummon();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Decline", ImVec2(130, 30))) {
|
|
|
|
|
|
gameHandler.declineSummon();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
void GameScreen::renderTradeRequestPopup(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.hasPendingTradeRequest()) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 370), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("Trade Request", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) {
|
|
|
|
|
|
ImGui::Text("%s wants to trade with you.", gameHandler.getTradePeerName().c_str());
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Button("Accept", ImVec2(130, 30))) {
|
|
|
|
|
|
gameHandler.acceptTradeRequest();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Decline", ImVec2(130, 30))) {
|
|
|
|
|
|
gameHandler.declineTradeRequest();
|
|
|
|
|
|
}
|
2026-03-09 13:58:02 -07:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 00:44:07 -07:00
|
|
|
|
void GameScreen::renderTradeWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isTradeOpen()) return;
|
|
|
|
|
|
|
|
|
|
|
|
const auto& mySlots = gameHandler.getMyTradeSlots();
|
|
|
|
|
|
const auto& peerSlots = gameHandler.getPeerTradeSlots();
|
|
|
|
|
|
const uint64_t myGold = gameHandler.getMyTradeGold();
|
|
|
|
|
|
const uint64_t peerGold = gameHandler.getPeerTradeGold();
|
|
|
|
|
|
const auto& peerName = gameHandler.getTradePeerName();
|
|
|
|
|
|
|
|
|
|
|
|
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 / 2.0f - 240.0f, screenH / 2.0f - 180.0f), ImGuiCond_Once);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(480.0f, 360.0f), ImGuiCond_Once);
|
|
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
|
if (ImGui::Begin(("Trade with " + peerName).c_str(), &open,
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) {
|
|
|
|
|
|
|
|
|
|
|
|
auto formatGold = [](uint64_t copper, char* buf, size_t bufsz) {
|
|
|
|
|
|
uint64_t g = copper / 10000;
|
|
|
|
|
|
uint64_t s = (copper % 10000) / 100;
|
|
|
|
|
|
uint64_t c = copper % 100;
|
|
|
|
|
|
if (g > 0) std::snprintf(buf, bufsz, "%llug %llus %lluc",
|
|
|
|
|
|
(unsigned long long)g, (unsigned long long)s, (unsigned long long)c);
|
|
|
|
|
|
else if (s > 0) std::snprintf(buf, bufsz, "%llus %lluc",
|
|
|
|
|
|
(unsigned long long)s, (unsigned long long)c);
|
|
|
|
|
|
else std::snprintf(buf, bufsz, "%lluc", (unsigned long long)c);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
auto renderSlotColumn = [&](const char* label,
|
|
|
|
|
|
const std::array<game::GameHandler::TradeSlot,
|
|
|
|
|
|
game::GameHandler::TRADE_SLOT_COUNT>& slots,
|
|
|
|
|
|
uint64_t gold, bool isMine) {
|
|
|
|
|
|
ImGui::Text("%s", label);
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < game::GameHandler::TRADE_SLOT_COUNT; ++i) {
|
|
|
|
|
|
const auto& slot = slots[i];
|
|
|
|
|
|
ImGui::PushID(i * (isMine ? 1 : -1) - (isMine ? 0 : 100));
|
|
|
|
|
|
|
|
|
|
|
|
if (slot.occupied && slot.itemId != 0) {
|
|
|
|
|
|
const auto* info = gameHandler.getItemInfo(slot.itemId);
|
|
|
|
|
|
std::string name = (info && info->valid && !info->name.empty())
|
|
|
|
|
|
? info->name
|
|
|
|
|
|
: ("Item " + std::to_string(slot.itemId));
|
|
|
|
|
|
if (slot.stackCount > 1)
|
|
|
|
|
|
name += " x" + std::to_string(slot.stackCount);
|
2026-03-11 20:42:26 -07:00
|
|
|
|
ImVec4 qc = (info && info->valid)
|
|
|
|
|
|
? InventoryScreen::getQualityColor(static_cast<game::ItemQuality>(info->quality))
|
|
|
|
|
|
: ImVec4(1.0f, 0.9f, 0.5f, 1.0f);
|
|
|
|
|
|
if (info && info->valid && info->displayInfoId != 0) {
|
|
|
|
|
|
VkDescriptorSet iconTex = inventoryScreen.getItemIcon(info->displayInfoId);
|
|
|
|
|
|
if (iconTex) {
|
|
|
|
|
|
ImGui::Image((ImTextureID)(uintptr_t)iconTex, ImVec2(16, 16));
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::TextColored(qc, "%d. %s", i + 1, name.c_str());
|
2026-03-11 00:44:07 -07:00
|
|
|
|
if (isMine && ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
|
|
|
|
|
|
gameHandler.clearTradeItem(static_cast<uint8_t>(i));
|
|
|
|
|
|
}
|
2026-03-11 21:32:54 -07:00
|
|
|
|
if (ImGui::IsItemHovered()) {
|
2026-03-11 20:42:26 -07:00
|
|
|
|
if (info && info->valid) inventoryScreen.renderItemTooltip(*info);
|
2026-03-11 21:32:54 -07:00
|
|
|
|
else if (isMine) ImGui::SetTooltip("Double-click to remove");
|
|
|
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
|
size_t curLen = strlen(chatInputBuffer);
|
|
|
|
|
|
if (curLen + link.size() + 1 < sizeof(chatInputBuffer)) {
|
|
|
|
|
|
strncat(chatInputBuffer, link.c_str(), sizeof(chatInputBuffer) - curLen - 1);
|
|
|
|
|
|
chatInputMoveCursorToEnd = true;
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
2026-03-11 00:44:07 -07:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextDisabled(" %d. (empty)", i + 1);
|
|
|
|
|
|
|
|
|
|
|
|
// Allow dragging inventory items into trade slots via right-click context menu
|
|
|
|
|
|
if (isMine && ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
|
|
|
|
|
ImGui::OpenPopup(("##additem" + std::to_string(i)).c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isMine) {
|
|
|
|
|
|
// Drag-from-inventory: show small popup listing bag items
|
|
|
|
|
|
if (ImGui::BeginPopup(("##additem" + std::to_string(i)).c_str())) {
|
|
|
|
|
|
ImGui::TextDisabled("Add from inventory:");
|
|
|
|
|
|
const auto& inv = gameHandler.getInventory();
|
|
|
|
|
|
// Backpack slots 0-15 (bag=255)
|
|
|
|
|
|
for (int si = 0; si < game::Inventory::BACKPACK_SLOTS; ++si) {
|
|
|
|
|
|
const auto& slot = inv.getBackpackSlot(si);
|
|
|
|
|
|
if (slot.empty()) continue;
|
|
|
|
|
|
const auto* ii = gameHandler.getItemInfo(slot.item.itemId);
|
|
|
|
|
|
std::string iname = (ii && ii->valid && !ii->name.empty())
|
|
|
|
|
|
? ii->name
|
|
|
|
|
|
: (!slot.item.name.empty() ? slot.item.name
|
|
|
|
|
|
: ("Item " + std::to_string(slot.item.itemId)));
|
|
|
|
|
|
if (ImGui::Selectable(iname.c_str())) {
|
|
|
|
|
|
// bag=255 = main backpack
|
|
|
|
|
|
gameHandler.setTradeItem(static_cast<uint8_t>(i), 255u,
|
|
|
|
|
|
static_cast<uint8_t>(si));
|
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Gold row
|
|
|
|
|
|
char gbuf[48];
|
|
|
|
|
|
formatGold(gold, gbuf, sizeof(gbuf));
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
if (isMine) {
|
|
|
|
|
|
ImGui::Text("Gold offered: %s", gbuf);
|
|
|
|
|
|
static char goldInput[32] = "0";
|
|
|
|
|
|
ImGui::SetNextItemWidth(120.0f);
|
|
|
|
|
|
if (ImGui::InputText("##goldset", goldInput, sizeof(goldInput),
|
|
|
|
|
|
ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_EnterReturnsTrue)) {
|
|
|
|
|
|
uint64_t copper = std::strtoull(goldInput, nullptr, 10);
|
|
|
|
|
|
gameHandler.setTradeGold(copper);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextDisabled("(copper, Enter to set)");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::Text("Gold offered: %s", gbuf);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Two-column layout: my offer | peer offer
|
|
|
|
|
|
float colW = ImGui::GetContentRegionAvail().x * 0.5f - 4.0f;
|
|
|
|
|
|
ImGui::BeginChild("##myoffer", ImVec2(colW, 240.0f), true);
|
|
|
|
|
|
renderSlotColumn("Your offer", mySlots, myGold, true);
|
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::BeginChild("##peroffer", ImVec2(colW, 240.0f), true);
|
|
|
|
|
|
renderSlotColumn((peerName + "'s offer").c_str(), peerSlots, peerGold, false);
|
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
|
|
|
|
|
|
// Buttons
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
float bw = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
|
|
|
|
|
if (ImGui::Button("Accept Trade", ImVec2(bw, 0))) {
|
|
|
|
|
|
gameHandler.acceptTrade();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(bw, 0))) {
|
|
|
|
|
|
gameHandler.cancelTrade();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
gameHandler.cancelTrade();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 14:01:27 -07:00
|
|
|
|
void GameScreen::renderLootRollPopup(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.hasPendingLootRoll()) return;
|
|
|
|
|
|
|
|
|
|
|
|
const auto& roll = gameHandler.getPendingLootRoll();
|
|
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 175, 310), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(350, 0), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("Loot Roll", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) {
|
|
|
|
|
|
// Quality color for item name
|
|
|
|
|
|
static const ImVec4 kQualityColors[] = {
|
|
|
|
|
|
ImVec4(0.6f, 0.6f, 0.6f, 1.0f), // 0=poor (grey)
|
|
|
|
|
|
ImVec4(1.0f, 1.0f, 1.0f, 1.0f), // 1=common (white)
|
|
|
|
|
|
ImVec4(0.1f, 1.0f, 0.1f, 1.0f), // 2=uncommon (green)
|
|
|
|
|
|
ImVec4(0.0f, 0.44f, 0.87f, 1.0f),// 3=rare (blue)
|
|
|
|
|
|
ImVec4(0.64f, 0.21f, 0.93f, 1.0f),// 4=epic (purple)
|
|
|
|
|
|
ImVec4(1.0f, 0.5f, 0.0f, 1.0f), // 5=legendary (orange)
|
2026-03-13 10:29:56 -07:00
|
|
|
|
ImVec4(0.90f, 0.80f, 0.50f, 1.0f),// 6=artifact (light gold)
|
|
|
|
|
|
ImVec4(0.90f, 0.80f, 0.50f, 1.0f),// 7=heirloom (light gold)
|
2026-03-09 14:01:27 -07:00
|
|
|
|
};
|
|
|
|
|
|
uint8_t q = roll.itemQuality;
|
2026-03-13 10:29:56 -07:00
|
|
|
|
ImVec4 col = (q < 8) ? kQualityColors[q] : kQualityColors[1];
|
2026-03-09 14:01:27 -07:00
|
|
|
|
|
2026-03-12 04:57:36 -07:00
|
|
|
|
// Countdown bar
|
|
|
|
|
|
{
|
|
|
|
|
|
auto now = std::chrono::steady_clock::now();
|
|
|
|
|
|
float elapsedMs = static_cast<float>(
|
|
|
|
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(now - roll.rollStartedAt).count());
|
|
|
|
|
|
float totalMs = static_cast<float>(roll.rollCountdownMs > 0 ? roll.rollCountdownMs : 60000);
|
|
|
|
|
|
float fraction = 1.0f - std::min(elapsedMs / totalMs, 1.0f);
|
|
|
|
|
|
float remainSec = (totalMs - elapsedMs) / 1000.0f;
|
|
|
|
|
|
if (remainSec < 0.0f) remainSec = 0.0f;
|
|
|
|
|
|
|
|
|
|
|
|
// Color: green → yellow → red
|
|
|
|
|
|
ImVec4 barColor;
|
|
|
|
|
|
if (fraction > 0.5f)
|
|
|
|
|
|
barColor = ImVec4(0.2f + (1.0f - fraction) * 1.4f, 0.85f, 0.2f, 1.0f);
|
|
|
|
|
|
else if (fraction > 0.2f)
|
|
|
|
|
|
barColor = ImVec4(1.0f, fraction * 1.7f, 0.1f, 1.0f);
|
|
|
|
|
|
else {
|
|
|
|
|
|
float pulse = 0.7f + 0.3f * std::sin(static_cast<float>(ImGui::GetTime()) * 6.0f);
|
|
|
|
|
|
barColor = ImVec4(pulse, 0.1f * pulse, 0.1f * pulse, 1.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
char timeBuf[16];
|
|
|
|
|
|
std::snprintf(timeBuf, sizeof(timeBuf), "%.0fs", remainSec);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, barColor);
|
|
|
|
|
|
ImGui::ProgressBar(fraction, ImVec2(-1, 12), timeBuf);
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 14:01:27 -07:00
|
|
|
|
ImGui::Text("An item is up for rolls:");
|
2026-03-10 20:59:02 -07:00
|
|
|
|
|
|
|
|
|
|
// Show item icon if available
|
|
|
|
|
|
const auto* rollInfo = gameHandler.getItemInfo(roll.itemId);
|
|
|
|
|
|
uint32_t rollDisplayId = rollInfo ? rollInfo->displayInfoId : 0;
|
|
|
|
|
|
VkDescriptorSet rollIcon = rollDisplayId ? inventoryScreen.getItemIcon(rollDisplayId) : VK_NULL_HANDLE;
|
|
|
|
|
|
if (rollIcon) {
|
|
|
|
|
|
ImGui::Image((ImTextureID)(uintptr_t)rollIcon, ImVec2(24, 24));
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
}
|
2026-03-13 05:23:31 -07:00
|
|
|
|
// Prefer live item info (arrives via SMSG_ITEM_QUERY_SINGLE_RESPONSE after the
|
|
|
|
|
|
// roll popup opens); fall back to the name cached at SMSG_LOOT_START_ROLL time.
|
|
|
|
|
|
const char* displayName = (rollInfo && rollInfo->valid && !rollInfo->name.empty())
|
|
|
|
|
|
? rollInfo->name.c_str()
|
|
|
|
|
|
: roll.itemName.c_str();
|
|
|
|
|
|
if (rollInfo && rollInfo->valid)
|
2026-03-13 10:30:54 -07:00
|
|
|
|
col = (rollInfo->quality < 8) ? kQualityColors[rollInfo->quality] : kQualityColors[1];
|
2026-03-13 05:23:31 -07:00
|
|
|
|
ImGui::TextColored(col, "[%s]", displayName);
|
2026-03-10 20:59:02 -07:00
|
|
|
|
if (ImGui::IsItemHovered() && rollInfo && rollInfo->valid) {
|
|
|
|
|
|
inventoryScreen.renderItemTooltip(*rollInfo);
|
|
|
|
|
|
}
|
2026-03-11 21:32:54 -07:00
|
|
|
|
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
|
|
|
|
|
ImGui::GetIO().KeyShift && rollInfo && rollInfo->valid && !rollInfo->name.empty()) {
|
|
|
|
|
|
std::string link = buildItemChatLink(rollInfo->entry, rollInfo->quality, rollInfo->name);
|
|
|
|
|
|
size_t curLen = strlen(chatInputBuffer);
|
|
|
|
|
|
if (curLen + link.size() + 1 < sizeof(chatInputBuffer)) {
|
|
|
|
|
|
strncat(chatInputBuffer, link.c_str(), sizeof(chatInputBuffer) - curLen - 1);
|
|
|
|
|
|
chatInputMoveCursorToEnd = true;
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-09 14:01:27 -07:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Button("Need", ImVec2(80, 30))) {
|
|
|
|
|
|
gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Greed", ImVec2(80, 30))) {
|
|
|
|
|
|
gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Disenchant", ImVec2(95, 30))) {
|
|
|
|
|
|
gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 2);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Pass", ImVec2(70, 30))) {
|
|
|
|
|
|
gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 96);
|
|
|
|
|
|
}
|
2026-03-12 08:59:38 -07:00
|
|
|
|
|
|
|
|
|
|
// Live roll results from group members
|
|
|
|
|
|
if (!roll.playerRolls.empty()) {
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::TextDisabled("Rolls so far:");
|
|
|
|
|
|
// Roll-type label + color
|
|
|
|
|
|
static const char* kRollLabels[] = {"Need", "Greed", "Disenchant", "Pass"};
|
|
|
|
|
|
static const ImVec4 kRollColors[] = {
|
|
|
|
|
|
ImVec4(0.2f, 0.9f, 0.2f, 1.0f), // Need — green
|
|
|
|
|
|
ImVec4(0.3f, 0.6f, 1.0f, 1.0f), // Greed — blue
|
|
|
|
|
|
ImVec4(0.7f, 0.3f, 0.9f, 1.0f), // Disenchant — purple
|
|
|
|
|
|
ImVec4(0.5f, 0.5f, 0.5f, 1.0f), // Pass — gray
|
|
|
|
|
|
};
|
|
|
|
|
|
auto rollTypeIndex = [](uint8_t t) -> int {
|
|
|
|
|
|
if (t == 0) return 0;
|
|
|
|
|
|
if (t == 1) return 1;
|
|
|
|
|
|
if (t == 2) return 2;
|
|
|
|
|
|
return 3; // pass (96 or unknown)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::BeginTable("##lootrolls", 3,
|
|
|
|
|
|
ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg)) {
|
|
|
|
|
|
ImGui::TableSetupColumn("Player", ImGuiTableColumnFlags_WidthStretch);
|
|
|
|
|
|
ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 72.0f);
|
|
|
|
|
|
ImGui::TableSetupColumn("Roll", ImGuiTableColumnFlags_WidthFixed, 32.0f);
|
|
|
|
|
|
for (const auto& r : roll.playerRolls) {
|
|
|
|
|
|
int ri = rollTypeIndex(r.rollType);
|
|
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
|
|
|
|
|
ImGui::TextUnformatted(r.playerName.c_str());
|
|
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
|
|
|
|
|
ImGui::TextColored(kRollColors[ri], "%s", kRollLabels[ri]);
|
|
|
|
|
|
ImGui::TableSetColumnIndex(2);
|
|
|
|
|
|
if (r.rollType != 96) {
|
|
|
|
|
|
ImGui::TextColored(kRollColors[ri], "%d", static_cast<int>(r.rollNum));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextDisabled("—");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTable();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-09 14:01:27 -07:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 21:39:48 -08:00
|
|
|
|
void GameScreen::renderGuildInvitePopup(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.hasPendingGuildInvite()) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 175, 250), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(350, 0), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("Guild Invite", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) {
|
|
|
|
|
|
ImGui::TextWrapped("%s has invited you to join %s.",
|
|
|
|
|
|
gameHandler.getPendingGuildInviterName().c_str(),
|
|
|
|
|
|
gameHandler.getPendingGuildInviteGuildName().c_str());
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Button("Accept", ImVec2(155, 30))) {
|
|
|
|
|
|
gameHandler.acceptGuildInvite();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Decline", ImVec2(155, 30))) {
|
|
|
|
|
|
gameHandler.declineGuildInvite();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 14:48:30 -07:00
|
|
|
|
void GameScreen::renderReadyCheckPopup(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.hasPendingReadyCheck()) 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 / 2 - 175, screenH / 2 - 60), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(350, 0), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("Ready Check", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) {
|
|
|
|
|
|
const std::string& initiator = gameHandler.getReadyCheckInitiator();
|
|
|
|
|
|
if (initiator.empty()) {
|
|
|
|
|
|
ImGui::Text("A ready check has been initiated!");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextWrapped("%s has initiated a ready check!", initiator.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Button("Ready", ImVec2(155, 30))) {
|
|
|
|
|
|
gameHandler.respondToReadyCheck(true);
|
|
|
|
|
|
gameHandler.dismissReadyCheck();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Not Ready", ImVec2(155, 30))) {
|
|
|
|
|
|
gameHandler.respondToReadyCheck(false);
|
|
|
|
|
|
gameHandler.dismissReadyCheck();
|
|
|
|
|
|
}
|
2026-03-12 09:07:37 -07:00
|
|
|
|
|
|
|
|
|
|
// Live player responses
|
|
|
|
|
|
const auto& results = gameHandler.getReadyCheckResults();
|
|
|
|
|
|
if (!results.empty()) {
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::BeginTable("##rcresults", 2,
|
|
|
|
|
|
ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg)) {
|
|
|
|
|
|
ImGui::TableSetupColumn("Player", ImGuiTableColumnFlags_WidthStretch);
|
|
|
|
|
|
ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 72.0f);
|
|
|
|
|
|
for (const auto& r : results) {
|
|
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
|
|
|
|
|
ImGui::TextUnformatted(r.name.c_str());
|
|
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
|
|
|
|
|
if (r.ready) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.2f, 0.9f, 0.2f, 1.0f), "Ready");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.9f, 0.3f, 0.3f, 1.0f), "Not Ready");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTable();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-09 14:48:30 -07:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 21:12:28 -07:00
|
|
|
|
void GameScreen::renderBgInvitePopup(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.hasPendingBgInvite()) return;
|
|
|
|
|
|
|
|
|
|
|
|
const auto& queues = gameHandler.getBgQueues();
|
|
|
|
|
|
// Find the first WAIT_JOIN slot
|
|
|
|
|
|
const game::GameHandler::BgQueueSlot* slot = nullptr;
|
|
|
|
|
|
for (const auto& s : queues) {
|
|
|
|
|
|
if (s.statusId == 2) { slot = &s; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!slot) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Compute time remaining
|
|
|
|
|
|
auto now = std::chrono::steady_clock::now();
|
|
|
|
|
|
double elapsed = std::chrono::duration<double>(now - slot->inviteReceivedTime).count();
|
|
|
|
|
|
double remaining = static_cast<double>(slot->inviteTimeout) - elapsed;
|
|
|
|
|
|
|
|
|
|
|
|
// If invite has expired, clear it silently (server will handle the queue)
|
|
|
|
|
|
if (remaining <= 0.0) {
|
|
|
|
|
|
gameHandler.declineBattlefield(slot->queueSlot);
|
|
|
|
|
|
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 / 2 - 190, screenH / 2 - 70), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(380, 0), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.18f, 0.95f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.4f, 0.4f, 1.0f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(0.15f, 0.15f, 0.4f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
|
|
|
|
|
|
|
|
|
|
|
|
const ImGuiWindowFlags popupFlags =
|
|
|
|
|
|
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse;
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("Battleground Ready!", nullptr, popupFlags)) {
|
|
|
|
|
|
// BG name
|
|
|
|
|
|
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 = "Alterac Valley"; break;
|
|
|
|
|
|
case 2: bgName = "Warsong Gulch"; break;
|
|
|
|
|
|
case 3: bgName = "Arathi Basin"; break;
|
|
|
|
|
|
case 7: bgName = "Eye of the Storm"; break;
|
|
|
|
|
|
case 9: bgName = "Strand of the Ancients"; break;
|
|
|
|
|
|
case 11: bgName = "Isle of Conquest"; break;
|
|
|
|
|
|
default: bgName = "Battleground"; break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.2f, 1.0f), "%s", bgName.c_str());
|
|
|
|
|
|
ImGui::TextWrapped("A spot has opened! You have %d seconds to enter.", static_cast<int>(remaining));
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
// Countdown progress bar
|
|
|
|
|
|
float frac = static_cast<float>(remaining / static_cast<double>(slot->inviteTimeout));
|
|
|
|
|
|
frac = std::clamp(frac, 0.0f, 1.0f);
|
|
|
|
|
|
ImVec4 barColor = frac > 0.5f ? ImVec4(0.2f, 0.8f, 0.2f, 1.0f)
|
|
|
|
|
|
: frac > 0.25f ? ImVec4(0.9f, 0.7f, 0.1f, 1.0f)
|
|
|
|
|
|
: ImVec4(0.9f, 0.2f, 0.2f, 1.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, barColor);
|
|
|
|
|
|
char countdownLabel[32];
|
|
|
|
|
|
snprintf(countdownLabel, sizeof(countdownLabel), "%ds", static_cast<int>(remaining));
|
|
|
|
|
|
ImGui::ProgressBar(frac, ImVec2(-1, 16), countdownLabel);
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.5f, 0.15f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.2f, 0.7f, 0.2f, 1.0f));
|
|
|
|
|
|
if (ImGui::Button("Enter Battleground", ImVec2(180, 30))) {
|
|
|
|
|
|
gameHandler.acceptBattlefield(slot->queueSlot);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.15f, 0.15f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.2f, 0.2f, 1.0f));
|
|
|
|
|
|
if (ImGui::Button("Leave Queue", ImVec2(175, 30))) {
|
|
|
|
|
|
gameHandler.declineBattlefield(slot->queueSlot);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
ImGui::PopStyleColor(3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 22:25:46 -07:00
|
|
|
|
void GameScreen::renderBfMgrInvitePopup(game::GameHandler& gameHandler) {
|
|
|
|
|
|
// Only shown on WotLK servers (outdoor battlefields like Wintergrasp use the BF Manager)
|
|
|
|
|
|
if (!gameHandler.hasBfMgrInvite()) 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 / 2.0f - 190.0f, screenH / 2.0f - 55.0f), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(380.0f, 0.0f), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.10f, 0.20f, 0.96f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.5f, 0.5f, 1.0f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(0.15f, 0.15f, 0.45f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
|
|
|
|
|
|
|
|
|
|
|
|
const ImGuiWindowFlags flags =
|
|
|
|
|
|
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse;
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("Battlefield", nullptr, flags)) {
|
|
|
|
|
|
// Resolve zone name for Wintergrasp (zoneId 4197)
|
|
|
|
|
|
uint32_t zoneId = gameHandler.getBfMgrZoneId();
|
|
|
|
|
|
const char* zoneName = nullptr;
|
|
|
|
|
|
if (zoneId == 4197) zoneName = "Wintergrasp";
|
|
|
|
|
|
else if (zoneId == 5095) zoneName = "Tol Barad";
|
|
|
|
|
|
|
|
|
|
|
|
if (zoneName) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.2f, 1.0f), "%s", zoneName);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.2f, 1.0f), "Outdoor Battlefield");
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::TextWrapped("You are invited to join the outdoor battlefield. Do you want to enter?");
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.5f, 0.15f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.2f, 0.7f, 0.2f, 1.0f));
|
|
|
|
|
|
if (ImGui::Button("Enter Battlefield", ImVec2(178, 28))) {
|
|
|
|
|
|
gameHandler.acceptBfMgrInvite();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.15f, 0.15f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.2f, 0.2f, 1.0f));
|
|
|
|
|
|
if (ImGui::Button("Decline", ImVec2(175, 28))) {
|
|
|
|
|
|
gameHandler.declineBfMgrInvite();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
ImGui::PopStyleColor(3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 21:40:21 -07:00
|
|
|
|
void GameScreen::renderLfgProposalPopup(game::GameHandler& gameHandler) {
|
|
|
|
|
|
using LfgState = game::GameHandler::LfgState;
|
|
|
|
|
|
if (gameHandler.getLfgState() != LfgState::Proposal) 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 / 2.0f - 175.0f, screenH / 2.0f - 65.0f), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(350.0f, 0.0f), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.14f, 0.08f, 0.96f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.3f, 0.8f, 0.3f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(0.1f, 0.3f, 0.1f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
|
|
|
|
|
|
|
|
|
|
|
|
const ImGuiWindowFlags flags =
|
|
|
|
|
|
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse;
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("Dungeon Finder", nullptr, flags)) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "A group has been found!");
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::TextWrapped("Please accept or decline to join the dungeon.");
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.5f, 0.15f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.2f, 0.7f, 0.2f, 1.0f));
|
|
|
|
|
|
if (ImGui::Button("Accept", ImVec2(155.0f, 30.0f))) {
|
|
|
|
|
|
gameHandler.lfgAcceptProposal(gameHandler.getLfgProposalId(), true);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.15f, 0.15f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.2f, 0.2f, 1.0f));
|
|
|
|
|
|
if (ImGui::Button("Decline", ImVec2(155.0f, 30.0f))) {
|
|
|
|
|
|
gameHandler.lfgAcceptProposal(gameHandler.getLfgProposalId(), false);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
ImGui::PopStyleColor(3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 21:39:48 -08:00
|
|
|
|
void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) {
|
2026-03-11 06:51:48 -07:00
|
|
|
|
// Guild Roster toggle (customizable keybind)
|
2026-03-14 03:49:42 -07:00
|
|
|
|
if (!chatInputActive && !ImGui::GetIO().WantTextInput &&
|
|
|
|
|
|
!ImGui::GetIO().WantCaptureKeyboard &&
|
|
|
|
|
|
KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_GUILD_ROSTER)) {
|
2026-02-13 21:39:48 -08:00
|
|
|
|
showGuildRoster_ = !showGuildRoster_;
|
|
|
|
|
|
if (showGuildRoster_) {
|
2026-03-10 05:46:03 -07:00
|
|
|
|
// Open friends tab directly if not in guild
|
2026-02-13 21:39:48 -08:00
|
|
|
|
if (!gameHandler.isInGuild()) {
|
2026-03-10 05:46:03 -07:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-02-14 15:05:18 -08:00
|
|
|
|
}
|
2026-03-10 05:46:03 -07:00
|
|
|
|
gameHandler.requestGuildRoster();
|
|
|
|
|
|
gameHandler.requestGuildInfo();
|
2026-02-14 15:05:18 -08:00
|
|
|
|
}
|
2026-02-25 14:44:44 -08: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();
|
|
|
|
|
|
uint32_t gold = cost / 10000;
|
|
|
|
|
|
uint32_t silver = (cost % 10000) / 100;
|
|
|
|
|
|
uint32_t copper = cost % 100;
|
2026-03-12 08:15:46 -07:00
|
|
|
|
ImGui::TextDisabled("Cost:"); ImGui::SameLine(0, 4);
|
|
|
|
|
|
renderCoinsText(gold, silver, copper);
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(120, 0))) {
|
|
|
|
|
|
petitionNameBuffer_[0] = '\0';
|
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
2026-02-13 21:39:48 -08:00
|
|
|
|
}
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImGui::EndPopup();
|
2026-02-13 21:39:48 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!showGuildRoster_) return;
|
|
|
|
|
|
|
2026-02-25 14:44:44 -08:00
|
|
|
|
// Get zone manager for name lookup
|
|
|
|
|
|
game::ZoneManager* zoneManager = nullptr;
|
|
|
|
|
|
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
|
|
|
|
|
zoneManager = renderer->getZoneManager();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 21:39:48 -08: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-16 20:16:14 -08:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 375, screenH / 2 - 250), ImGuiCond_Once);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(750, 500), ImGuiCond_Once);
|
2026-02-13 21:39:48 -08:00
|
|
|
|
|
2026-03-10 05:46:03 -07:00
|
|
|
|
std::string title = gameHandler.isInGuild() ? (gameHandler.getGuildName() + " - Social") : "Social";
|
2026-02-13 21:39:48 -08:00
|
|
|
|
bool open = showGuildRoster_;
|
|
|
|
|
|
if (ImGui::Begin(title.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
|
2026-02-25 14:44:44 -08:00
|
|
|
|
// 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-02-13 21:39:48 -08:00
|
|
|
|
|
2026-02-25 14:44:44 -08: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-02-13 21:39:48 -08:00
|
|
|
|
|
2026-02-25 14:44:44 -08:00
|
|
|
|
// Count online
|
|
|
|
|
|
int onlineCount = 0;
|
|
|
|
|
|
for (const auto& m : roster.members) {
|
|
|
|
|
|
if (m.online) ++onlineCount;
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Text("%d members (%d online)", (int)roster.members.size(), onlineCount);
|
|
|
|
|
|
ImGui::Separator();
|
2026-02-13 21:39:48 -08:00
|
|
|
|
|
2026-02-25 14:44:44 -08:00
|
|
|
|
const auto& rankNames = gameHandler.getGuildRankNames();
|
|
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
for (const auto& m : sortedMembers) {
|
|
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
|
|
ImVec4 textColor = m.online ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f)
|
|
|
|
|
|
: ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
|
2026-03-12 08:42:55 -07:00
|
|
|
|
ImVec4 nameColor = m.online ? classColorVec4(m.classId) : textColor;
|
2026-02-13 21:39:48 -08:00
|
|
|
|
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImGui::TableNextColumn();
|
2026-03-12 08:42:55 -07:00
|
|
|
|
ImGui::TextColored(nameColor, "%s", m.name.c_str());
|
2026-02-13 21:39:48 -08:00
|
|
|
|
|
2026-02-25 14:44:44 -08:00
|
|
|
|
// Right-click context menu
|
|
|
|
|
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
|
|
|
|
|
selectedGuildMember_ = m.name;
|
|
|
|
|
|
ImGui::OpenPopup("GuildMemberContext");
|
|
|
|
|
|
}
|
2026-02-16 20:16:14 -08:00
|
|
|
|
|
2026-02-25 14:44:44 -08: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-02-13 21:39:48 -08:00
|
|
|
|
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
|
|
ImGui::TextColored(textColor, "%u", m.level);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TableNextColumn();
|
2026-03-12 08:46:26 -07:00
|
|
|
|
const char* className = classNameStr(m.classId);
|
2026-03-12 08:42:55 -07:00
|
|
|
|
ImVec4 classCol = m.online ? classColorVec4(m.classId) : textColor;
|
2026-03-12 04:46:25 -07:00
|
|
|
|
ImGui::TextColored(classCol, "%s", className);
|
2026-02-25 14:44:44 -08: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-02-13 21:39:48 -08:00
|
|
|
|
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
|
|
ImGui::TextColored(textColor, "%s", m.publicNote.c_str());
|
2026-02-13 21:39:48 -08:00
|
|
|
|
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
|
|
ImGui::TextColored(textColor, "%s", m.officerNote.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTable();
|
|
|
|
|
|
}
|
2026-02-13 21:39:48 -08:00
|
|
|
|
|
2026-02-25 14:44:44 -08:00
|
|
|
|
// Context menu popup
|
|
|
|
|
|
if (ImGui::BeginPopup("GuildMemberContext")) {
|
2026-03-11 23:48:07 -07:00
|
|
|
|
ImGui::TextDisabled("%s", selectedGuildMember_.c_str());
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImGui::Separator();
|
2026-03-11 23:48:07 -07:00
|
|
|
|
// Social actions — only for online members
|
|
|
|
|
|
bool memberOnline = false;
|
|
|
|
|
|
for (const auto& mem : roster.members) {
|
|
|
|
|
|
if (mem.name == selectedGuildMember_) { memberOnline = mem.online; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (memberOnline) {
|
|
|
|
|
|
if (ImGui::MenuItem("Whisper")) {
|
|
|
|
|
|
selectedChatType = 4;
|
|
|
|
|
|
strncpy(whisperTargetBuffer, selectedGuildMember_.c_str(),
|
|
|
|
|
|
sizeof(whisperTargetBuffer) - 1);
|
|
|
|
|
|
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Invite to Group")) {
|
|
|
|
|
|
gameHandler.inviteToGroup(selectedGuildMember_);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
}
|
2026-03-12 00:26:47 -07:00
|
|
|
|
if (!selectedGuildMember_.empty()) {
|
|
|
|
|
|
if (ImGui::MenuItem("Add Friend"))
|
|
|
|
|
|
gameHandler.addFriend(selectedGuildMember_);
|
|
|
|
|
|
if (ImGui::MenuItem("Ignore"))
|
|
|
|
|
|
gameHandler.addIgnore(selectedGuildMember_);
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
}
|
2026-02-25 14:44:44 -08:00
|
|
|
|
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-02-16 20:16:14 -08:00
|
|
|
|
|
2026-02-25 14:44:44 -08:00
|
|
|
|
// Note edit modal
|
|
|
|
|
|
if (showGuildNoteEdit_) {
|
|
|
|
|
|
ImGui::OpenPopup("EditGuildNote");
|
|
|
|
|
|
showGuildNoteEdit_ = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
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_);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Cancel")) {
|
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
2026-02-13 21:39:48 -08:00
|
|
|
|
}
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImGui::EndTabItem();
|
2026-02-13 21:39:48 -08:00
|
|
|
|
}
|
2026-02-16 20:16:14 -08:00
|
|
|
|
|
2026-02-25 14:44:44 -08: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();
|
|
|
|
|
|
|
|
|
|
|
|
// Guild name (large, gold)
|
|
|
|
|
|
ImGui::PushFont(nullptr); // default font
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "<%s>", gameHandler.getGuildName().c_str());
|
|
|
|
|
|
ImGui::PopFont();
|
2026-02-16 20:16:14 -08:00
|
|
|
|
ImGui::Separator();
|
2026-02-25 14:44:44 -08: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);
|
2026-02-16 20:16:14 -08:00
|
|
|
|
}
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
// Guild description / info text
|
|
|
|
|
|
if (!roster.guildInfo.empty()) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), "Description:");
|
|
|
|
|
|
ImGui::TextWrapped("%s", roster.guildInfo.c_str());
|
2026-02-16 20:16:14 -08:00
|
|
|
|
}
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
// 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(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "(not set)");
|
2026-02-16 20:16:14 -08:00
|
|
|
|
}
|
2026-02-25 14:44:44 -08:00
|
|
|
|
if (ImGui::Button("Set MOTD")) {
|
|
|
|
|
|
showMotdEdit_ = true;
|
|
|
|
|
|
snprintf(guildMotdEditBuffer_, sizeof(guildMotdEditBuffer_), "%s", roster.motd.c_str());
|
2026-02-16 20:16:14 -08:00
|
|
|
|
}
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
// 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();
|
2026-02-16 20:16:14 -08:00
|
|
|
|
}
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(120, 0))) {
|
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndPopup();
|
2026-02-16 20:16:14 -08:00
|
|
|
|
}
|
2026-02-25 14:44:44 -08: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-02-16 20:16:14 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 14:44:44 -08:00
|
|
|
|
// Rank list
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "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(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "[%s]", perms.c_str());
|
|
|
|
|
|
}
|
2026-02-16 20:16:14 -08:00
|
|
|
|
} else {
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImGui::Text(" %zu. %s", i + 1, rankNames[i].c_str());
|
2026-02-16 20:16:14 -08:00
|
|
|
|
}
|
2026-02-25 14:44:44 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Rank management buttons
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
if (ImGui::Button("Add Rank")) {
|
|
|
|
|
|
showAddRankModal_ = true;
|
|
|
|
|
|
addRankNameBuffer_[0] = '\0';
|
2026-02-16 20:16:14 -08:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
2026-02-25 14:44:44 -08:00
|
|
|
|
if (ImGui::Button("Delete Last Rank")) {
|
|
|
|
|
|
gameHandler.deleteGuildRank();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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-02-16 20:16:14 -08:00
|
|
|
|
}
|
2026-02-25 14:44:44 -08:00
|
|
|
|
|
|
|
|
|
|
ImGui::EndTabItem();
|
2026-02-16 20:16:14 -08:00
|
|
|
|
}
|
2026-02-25 14:44:44 -08:00
|
|
|
|
|
2026-03-10 05:46:03 -07:00
|
|
|
|
// ---- Friends tab ----
|
|
|
|
|
|
if (ImGui::BeginTabItem("Friends")) {
|
|
|
|
|
|
guildRosterTab_ = 2;
|
|
|
|
|
|
const auto& contacts = gameHandler.getContacts();
|
|
|
|
|
|
|
2026-03-11 21:53:15 -07: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();
|
|
|
|
|
|
|
|
|
|
|
|
// Note-edit state
|
|
|
|
|
|
static std::string friendNoteTarget;
|
|
|
|
|
|
static char friendNoteBuf[256] = {};
|
|
|
|
|
|
static bool openNotePopup = false;
|
|
|
|
|
|
|
2026-03-10 05:46:03 -07:00
|
|
|
|
// Filter to friends only
|
|
|
|
|
|
int friendCount = 0;
|
2026-03-11 21:53:15 -07:00
|
|
|
|
for (size_t ci = 0; ci < contacts.size(); ++ci) {
|
|
|
|
|
|
const auto& c = contacts[ci];
|
2026-03-10 05:46:03 -07:00
|
|
|
|
if (!c.isFriend()) continue;
|
|
|
|
|
|
++friendCount;
|
|
|
|
|
|
|
2026-03-11 21:53:15 -07:00
|
|
|
|
ImGui::PushID(static_cast<int>(ci));
|
|
|
|
|
|
|
2026-03-10 05:46:03 -07:00
|
|
|
|
// 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();
|
|
|
|
|
|
|
2026-03-11 21:53:15 -07:00
|
|
|
|
// Name as Selectable for right-click context menu
|
2026-03-10 05:46:03 -07:00
|
|
|
|
const char* displayName = c.name.empty() ? "(unknown)" : c.name.c_str();
|
|
|
|
|
|
ImVec4 nameCol = c.isOnline()
|
|
|
|
|
|
? ImVec4(1.0f, 1.0f, 1.0f, 1.0f)
|
|
|
|
|
|
: ImVec4(0.55f, 0.55f, 0.55f, 1.0f);
|
2026-03-11 21:53:15 -07:00
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, nameCol);
|
|
|
|
|
|
ImGui::Selectable(displayName, false, ImGuiSelectableFlags_AllowOverlap, ImVec2(130.0f, 0.0f));
|
|
|
|
|
|
ImGui::PopStyleColor();
|
2026-03-10 05:46:03 -07:00
|
|
|
|
|
2026-03-11 21:53:15 -07:00
|
|
|
|
// Double-click to whisper
|
|
|
|
|
|
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)
|
|
|
|
|
|
&& !c.name.empty()) {
|
|
|
|
|
|
selectedChatType = 4;
|
|
|
|
|
|
strncpy(whisperTargetBuffer, c.name.c_str(), sizeof(whisperTargetBuffer) - 1);
|
|
|
|
|
|
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Right-click context menu
|
|
|
|
|
|
if (ImGui::BeginPopupContextItem("FriendCtx")) {
|
|
|
|
|
|
ImGui::TextDisabled("%s", displayName);
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Whisper") && !c.name.empty()) {
|
|
|
|
|
|
selectedChatType = 4;
|
|
|
|
|
|
strncpy(whisperTargetBuffer, c.name.c_str(), sizeof(whisperTargetBuffer) - 1);
|
|
|
|
|
|
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
2026-03-12 00:02:20 -07:00
|
|
|
|
if (c.isOnline() && ImGui::MenuItem("Invite to Group") && !c.name.empty()) {
|
|
|
|
|
|
gameHandler.inviteToGroup(c.name);
|
|
|
|
|
|
}
|
2026-03-11 21:53:15 -07:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Note tooltip on hover
|
|
|
|
|
|
if (ImGui::IsItemHovered() && !c.note.empty()) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
ImGui::TextDisabled("Note: %s", c.note.c_str());
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 07:39:11 -07:00
|
|
|
|
// Level, class, and status
|
2026-03-10 05:46:03 -07:00
|
|
|
|
if (c.isOnline()) {
|
2026-03-12 07:39:11 -07:00
|
|
|
|
ImGui::SameLine(150.0f);
|
2026-03-10 05:46:03 -07:00
|
|
|
|
const char* statusLabel =
|
2026-03-11 21:53:15 -07:00
|
|
|
|
(c.status == 2) ? " (AFK)" :
|
|
|
|
|
|
(c.status == 3) ? " (DND)" : "";
|
2026-03-12 07:39:11 -07:00
|
|
|
|
// Class color for the level/class display
|
2026-03-12 08:46:26 -07:00
|
|
|
|
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) {
|
2026-03-12 07:39:11 -07:00
|
|
|
|
ImGui::TextColored(friendClassCol, "Lv%u %s%s", c.level, friendClassName, statusLabel);
|
|
|
|
|
|
} else if (c.level > 0) {
|
2026-03-11 21:53:15 -07:00
|
|
|
|
ImGui::TextDisabled("Lv %u%s", c.level, statusLabel);
|
2026-03-10 05:46:03 -07:00
|
|
|
|
} else if (*statusLabel) {
|
2026-03-11 21:53:15 -07:00
|
|
|
|
ImGui::TextDisabled("%s", statusLabel + 1);
|
2026-03-10 05:46:03 -07:00
|
|
|
|
}
|
2026-03-12 07:39:11 -07: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-03-10 05:46:03 -07:00
|
|
|
|
}
|
2026-03-11 21:53:15 -07:00
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
2026-03-10 05:46:03 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (friendCount == 0) {
|
2026-03-11 21:53:15 -07:00
|
|
|
|
ImGui::TextDisabled("No friends found.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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-10 05:46:03 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 21:53:15 -07:00
|
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- Ignore List tab ----
|
|
|
|
|
|
if (ImGui::BeginTabItem("Ignore")) {
|
|
|
|
|
|
guildRosterTab_ = 3;
|
|
|
|
|
|
const auto& contacts = gameHandler.getContacts();
|
|
|
|
|
|
|
|
|
|
|
|
// 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';
|
|
|
|
|
|
}
|
2026-03-10 05:46:03 -07:00
|
|
|
|
ImGui::Separator();
|
2026-03-11 21:53:15 -07: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();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (ignoreCount == 0) {
|
|
|
|
|
|
ImGui::TextDisabled("Ignore list is empty.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 05:46:03 -07:00
|
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImGui::EndTabBar();
|
2026-02-13 21:39:48 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
showGuildRoster_ = open;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 00:53:57 -07:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Social Frame — compact online friends panel (toggled by showSocialFrame_)
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderSocialFrame(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!showSocialFrame_) return;
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW - 230.0f, 240.0f), ImGuiCond_Once);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(220.0f, 0.0f), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.92f));
|
|
|
|
|
|
|
2026-03-12 10:01:35 -07:00
|
|
|
|
// State for "Set Note" inline editing
|
|
|
|
|
|
static int noteEditContactIdx = -1;
|
|
|
|
|
|
static char noteEditBuf[128] = {};
|
|
|
|
|
|
|
2026-03-12 00:53:57 -07:00
|
|
|
|
bool open = showSocialFrame_;
|
2026-03-12 01:31:44 -07:00
|
|
|
|
char socialTitle[32];
|
|
|
|
|
|
snprintf(socialTitle, sizeof(socialTitle), "Social (%d online)##SocialFrame", onlineCount);
|
|
|
|
|
|
if (ImGui::Begin(socialTitle, &open,
|
2026-03-12 00:53:57 -07:00
|
|
|
|
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
|
|
|
|
|
|
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar)) {
|
|
|
|
|
|
|
2026-03-12 07:25:56 -07: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 01:31:44 -07:00
|
|
|
|
if (ImGui::BeginTabBar("##SocialTabs")) {
|
|
|
|
|
|
// ---- Friends tab ----
|
|
|
|
|
|
if (ImGui::BeginTabItem("Friends")) {
|
|
|
|
|
|
ImGui::BeginChild("##FriendsList", ImVec2(200, 200), false);
|
|
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(ci));
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
const char* displayName = c.name.empty() ? "(unknown)" : c.name.c_str();
|
|
|
|
|
|
ImVec4 nameCol = c.isOnline()
|
2026-03-12 08:44:08 -07:00
|
|
|
|
? classColorVec4(static_cast<uint8_t>(c.classId))
|
2026-03-12 01:31:44 -07:00
|
|
|
|
: ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
|
|
|
|
|
|
ImGui::TextColored(nameCol, "%s", displayName);
|
|
|
|
|
|
|
|
|
|
|
|
if (c.isOnline() && c.level > 0) {
|
|
|
|
|
|
ImGui::SameLine();
|
2026-03-12 07:25:56 -07:00
|
|
|
|
// Show level and class name in class color
|
2026-03-12 08:46:26 -07:00
|
|
|
|
ImGui::TextColored(classColorVec4(static_cast<uint8_t>(c.classId)),
|
|
|
|
|
|
"Lv%u %s", c.level, classNameStr(static_cast<uint8_t>(c.classId)));
|
2026-03-12 07:25:56 -07: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 01:31:44 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Right-click context menu
|
|
|
|
|
|
if (ImGui::BeginPopupContextItem("FriendCtx")) {
|
|
|
|
|
|
ImGui::TextDisabled("%s", displayName);
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (c.isOnline()) {
|
|
|
|
|
|
if (ImGui::MenuItem("Whisper")) {
|
|
|
|
|
|
showSocialFrame_ = false;
|
|
|
|
|
|
strncpy(whisperTargetBuffer, c.name.c_str(), sizeof(whisperTargetBuffer) - 1);
|
|
|
|
|
|
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
|
|
|
|
|
selectedChatType = 4;
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Invite to Group"))
|
|
|
|
|
|
gameHandler.inviteToGroup(c.name);
|
2026-03-12 10:01:35 -07:00
|
|
|
|
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");
|
2026-03-12 01:31:44 -07:00
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Remove Friend"))
|
|
|
|
|
|
gameHandler.removeFriend(c.name);
|
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
2026-03-12 00:53:57 -07:00
|
|
|
|
|
2026-03-12 01:31:44 -07:00
|
|
|
|
++shown;
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
// Separator between online and offline if there are both
|
|
|
|
|
|
if (pass == 0 && shown > 0) {
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (shown == 0) {
|
|
|
|
|
|
ImGui::TextDisabled("No friends yet.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::EndChild();
|
2026-03-12 10:01:35 -07: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-12 01:31:44 -07:00
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
// Add friend
|
|
|
|
|
|
static char addFriendBuf[64] = {};
|
|
|
|
|
|
ImGui::SetNextItemWidth(140.0f);
|
|
|
|
|
|
ImGui::InputText("##sf_addfriend", addFriendBuf, sizeof(addFriendBuf));
|
2026-03-12 00:53:57 -07:00
|
|
|
|
ImGui::SameLine();
|
2026-03-12 01:31:44 -07:00
|
|
|
|
if (ImGui::Button("+##addfriend") && addFriendBuf[0] != '\0') {
|
|
|
|
|
|
gameHandler.addFriend(addFriendBuf);
|
|
|
|
|
|
addFriendBuf[0] = '\0';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::EndTabItem();
|
2026-03-12 00:53:57 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 01:31:44 -07:00
|
|
|
|
// ---- Ignore tab ----
|
|
|
|
|
|
if (ImGui::BeginTabItem("Ignore")) {
|
|
|
|
|
|
const auto& ignores = gameHandler.getIgnoreCache();
|
|
|
|
|
|
ImGui::BeginChild("##IgnoreList", ImVec2(200, 200), false);
|
|
|
|
|
|
|
|
|
|
|
|
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-12 00:53:57 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 01:31:44 -07:00
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
ImGui::Separator();
|
2026-03-12 00:53:57 -07:00
|
|
|
|
|
2026-03-12 01:31:44 -07: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-12 00:53:57 -07:00
|
|
|
|
|
2026-03-12 01:31:44 -07:00
|
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 01:51:18 -07:00
|
|
|
|
// ---- Channels tab ----
|
|
|
|
|
|
if (ImGui::BeginTabItem("Channels")) {
|
|
|
|
|
|
const auto& channels = gameHandler.getJoinedChannels();
|
|
|
|
|
|
ImGui::BeginChild("##ChannelList", ImVec2(200, 200), false);
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
// 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';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 21:01:51 -07:00
|
|
|
|
// ---- Arena tab (WotLK: shows per-team rating/record + roster) ----
|
2026-03-12 14:58:48 -07:00
|
|
|
|
const auto& arenaStats = gameHandler.getArenaTeamStats();
|
|
|
|
|
|
if (!arenaStats.empty()) {
|
|
|
|
|
|
if (ImGui::BeginTabItem("Arena")) {
|
2026-03-12 21:01:51 -07:00
|
|
|
|
ImGui::BeginChild("##ArenaList", ImVec2(0, 0), false);
|
2026-03-12 14:58:48 -07:00
|
|
|
|
|
|
|
|
|
|
for (size_t ai = 0; ai < arenaStats.size(); ++ai) {
|
|
|
|
|
|
const auto& ts = arenaStats[ai];
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(ai));
|
|
|
|
|
|
|
|
|
|
|
|
// Team header with rating
|
|
|
|
|
|
char teamLabel[48];
|
|
|
|
|
|
snprintf(teamLabel, sizeof(teamLabel), "Team #%u", ts.teamId);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.2f, 1.0f), "%s", teamLabel);
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Weekly record
|
|
|
|
|
|
uint32_t weekLosses = ts.weekGames > ts.weekWins
|
|
|
|
|
|
? ts.weekGames - ts.weekWins : 0;
|
|
|
|
|
|
ImGui::Text("Week: %u W / %u L", ts.weekWins, weekLosses);
|
|
|
|
|
|
|
|
|
|
|
|
// 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-12 21:01:51 -07: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());
|
|
|
|
|
|
// 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();
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Text("%u", m.personalRating);
|
|
|
|
|
|
ImGui::NextColumn();
|
|
|
|
|
|
|
|
|
|
|
|
uint32_t wL = m.weekGames > m.weekWins
|
|
|
|
|
|
? m.weekGames - m.weekWins : 0;
|
|
|
|
|
|
ImGui::Text("%uW/%uL", m.weekWins, wL);
|
|
|
|
|
|
ImGui::NextColumn();
|
|
|
|
|
|
|
|
|
|
|
|
uint32_t sL = m.seasonGames > m.seasonWins
|
|
|
|
|
|
? m.seasonGames - m.seasonWins : 0;
|
|
|
|
|
|
ImGui::Text("%uW/%uL", m.seasonWins, sL);
|
|
|
|
|
|
ImGui::NextColumn();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Columns(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:58:48 -07:00
|
|
|
|
ImGui::Unindent(8.0f);
|
|
|
|
|
|
|
|
|
|
|
|
if (ai + 1 < arenaStats.size())
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 01:31:44 -07:00
|
|
|
|
ImGui::EndTabBar();
|
2026-03-12 00:53:57 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
showSocialFrame_ = open;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
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
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Buff/Debuff Bar (Phase 3)
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderBuffBar(game::GameHandler& gameHandler) {
|
|
|
|
|
|
const auto& auras = gameHandler.getPlayerAuras();
|
|
|
|
|
|
|
|
|
|
|
|
// Count non-empty auras
|
|
|
|
|
|
int activeCount = 0;
|
|
|
|
|
|
for (const auto& a : auras) {
|
|
|
|
|
|
if (!a.isEmpty()) activeCount++;
|
|
|
|
|
|
}
|
2026-02-26 10:41:29 -08:00
|
|
|
|
if (activeCount == 0 && !gameHandler.hasPet()) 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-02-07 23:47:43 -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-10 21:32:58 -07: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
|
2026-02-07 23:47:43 -08:00
|
|
|
|
constexpr float ICON_SIZE = 32.0f;
|
2026-03-10 21:32:58 -07:00
|
|
|
|
constexpr int ICONS_PER_ROW = 8;
|
2026-02-07 23:47:43 -08:00
|
|
|
|
float barW = ICONS_PER_ROW * (ICON_SIZE + 4.0f) + 8.0f;
|
2026-03-10 21:29:47 -07:00
|
|
|
|
ImVec2 displaySize = ImGui::GetIO().DisplaySize;
|
|
|
|
|
|
float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f;
|
2026-03-10 21:32:58 -07:00
|
|
|
|
// 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);
|
2026-02-07 23:47:43 -08:00
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(barW, 0), 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
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
2026-02-07 23:47:43 -08:00
|
|
|
|
ImGuiWindowFlags_AlwaysAutoResize | 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::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
|
2026-02-07 23:47:43 -08:00
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2.0f, 2.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
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##BuffBar", nullptr, flags)) {
|
2026-03-12 06:08:26 -07:00
|
|
|
|
// 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-03-10 21:29:47 -07:00
|
|
|
|
// Render one pass for buffs, one for debuffs
|
|
|
|
|
|
for (int pass = 0; pass < 2; ++pass) {
|
|
|
|
|
|
bool wantBuff = (pass == 0);
|
|
|
|
|
|
int shown = 0;
|
2026-03-12 06:08:26 -07:00
|
|
|
|
for (size_t si = 0; si < buffSortedIdx.size() && shown < 40; ++si) {
|
|
|
|
|
|
size_t i = buffSortedIdx[si];
|
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& aura = auras[i];
|
|
|
|
|
|
if (aura.isEmpty()) continue;
|
|
|
|
|
|
|
2026-03-10 21:29:47 -07:00
|
|
|
|
bool isBuff = (aura.flags & 0x80) == 0; // 0x80 = negative/debuff flag
|
|
|
|
|
|
if (isBuff != wantBuff) continue; // only render matching pass
|
|
|
|
|
|
|
2026-02-07 23:47:43 -08:00
|
|
|
|
if (shown > 0 && shown % ICONS_PER_ROW != 0) ImGui::SameLine();
|
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 21:29:47 -07:00
|
|
|
|
ImGui::PushID(static_cast<int>(i) + (pass * 256));
|
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 06:55:16 -07: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-07 23:47:43 -08:00
|
|
|
|
|
|
|
|
|
|
// Try to get spell icon
|
2026-02-22 03:32:08 -08:00
|
|
|
|
VkDescriptorSet iconTex = VK_NULL_HANDLE;
|
2026-02-07 23:47:43 -08:00
|
|
|
|
if (assetMgr) {
|
|
|
|
|
|
iconTex = getSpellIcon(aura.spellId, assetMgr);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (iconTex) {
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, borderColor);
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2));
|
2026-02-08 00:00:12 -08:00
|
|
|
|
ImGui::ImageButton("##aura",
|
2026-02-07 23:47:43 -08:00
|
|
|
|
(ImTextureID)(uintptr_t)iconTex,
|
|
|
|
|
|
ImVec2(ICON_SIZE - 4, ICON_SIZE - 4));
|
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();
|
2026-02-07 23:47:43 -08:00
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, borderColor);
|
|
|
|
|
|
char label[8];
|
|
|
|
|
|
snprintf(label, sizeof(label), "%u", aura.spellId);
|
2026-02-08 00:00:12 -08:00
|
|
|
|
ImGui::Button(label, ImVec2(ICON_SIZE, ICON_SIZE));
|
2026-02-07 23:47:43 -08:00
|
|
|
|
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-09 16:36:58 -07: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-03-17 19:04:40 -07: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);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::GetWindowDrawList()->AddConvexPolyFilled(
|
|
|
|
|
|
pts, SWEEP_SEGS + 2, IM_COL32(0, 0, 0, 145));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 16:36:58 -07: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;
|
2026-03-12 04:34:00 -07:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|
2026-03-09 16:36:58 -07:00
|
|
|
|
// 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),
|
2026-03-12 04:34:00 -07:00
|
|
|
|
timerColor, timeStr);
|
2026-03-09 16:36:58 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 17:08:14 -07: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-08 00:00:12 -08:00
|
|
|
|
// Right-click to cancel buffs / dismount
|
2026-02-13 22:51:49 -08:00
|
|
|
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
2026-02-08 00:00:12 -08:00
|
|
|
|
if (gameHandler.isMounted()) {
|
|
|
|
|
|
gameHandler.dismount();
|
2026-02-13 22:51:49 -08:00
|
|
|
|
} else if (isBuff) {
|
2026-02-08 00:00:12 -08:00
|
|
|
|
gameHandler.cancelAura(aura.spellId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 19:33:25 -07:00
|
|
|
|
// Tooltip: rich spell info + remaining duration
|
2026-02-07 23:47:43 -08:00
|
|
|
|
if (ImGui::IsItemHovered()) {
|
2026-03-10 19:33:25 -07:00
|
|
|
|
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-03-09 16:36:58 -07:00
|
|
|
|
if (remainMs > 0) {
|
|
|
|
|
|
int seconds = remainMs / 1000;
|
2026-03-10 19:33:25 -07:00
|
|
|
|
char durBuf[32];
|
|
|
|
|
|
if (seconds < 60) snprintf(durBuf, sizeof(durBuf), "Remaining: %ds", seconds);
|
|
|
|
|
|
else snprintf(durBuf, sizeof(durBuf), "Remaining: %dm %ds", seconds / 60, seconds % 60);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", durBuf);
|
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 19:33:25 -07:00
|
|
|
|
ImGui::EndTooltip();
|
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::PopID();
|
|
|
|
|
|
shown++;
|
2026-03-10 21:29:47 -07:00
|
|
|
|
} // end aura loop
|
|
|
|
|
|
// Add visual gap between buffs and debuffs
|
|
|
|
|
|
if (pass == 0 && shown > 0) ImGui::Spacing();
|
|
|
|
|
|
} // end pass loop
|
|
|
|
|
|
|
2026-02-26 10:41:29 -08:00
|
|
|
|
// Dismiss Pet button
|
|
|
|
|
|
if (gameHandler.hasPet()) {
|
2026-03-10 21:29:47 -07:00
|
|
|
|
ImGui::Spacing();
|
2026-02-26 10:41:29 -08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-03-12 18:15:51 -07:00
|
|
|
|
|
|
|
|
|
|
// Temporary weapon enchant timers (Shaman imbues, Rogue poisons, whetstones, etc.)
|
|
|
|
|
|
{
|
|
|
|
|
|
const auto& timers = gameHandler.getTempEnchantTimers();
|
|
|
|
|
|
if (!timers.empty()) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
static const ImVec4 kEnchantSlotColors[] = {
|
|
|
|
|
|
ImVec4(0.9f, 0.6f, 0.1f, 1.0f), // 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());
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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();
|
|
|
|
|
|
|
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
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Loot Window (Phase 5)
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderLootWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isLootWindowOpen()) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
|
2026-02-06 15:18:50 -08:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 200), 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
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
|
if (ImGui::Begin("Loot", &open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
|
|
|
|
const auto& loot = gameHandler.getCurrentLoot();
|
|
|
|
|
|
|
2026-03-12 08:13:03 -07:00
|
|
|
|
// Gold (auto-looted on open; shown for feedback)
|
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
|
|
|
|
if (loot.gold > 0) {
|
2026-03-12 08:13:03 -07:00
|
|
|
|
ImGui::TextDisabled("Gold:");
|
|
|
|
|
|
ImGui::SameLine(0, 4);
|
|
|
|
|
|
renderCoinsText(loot.getGold(), loot.getSilver(), loot.getCopper());
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 15:41:29 -08:00
|
|
|
|
// Items with icons and labels
|
|
|
|
|
|
constexpr float iconSize = 32.0f;
|
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
|
|
|
|
int lootSlotClicked = -1; // defer loot pickup to avoid iterator invalidation
|
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& item : loot.items) {
|
|
|
|
|
|
ImGui::PushID(item.slotIndex);
|
2026-02-06 15:41:29 -08:00
|
|
|
|
|
|
|
|
|
|
// Get item info for name and quality
|
|
|
|
|
|
const auto* info = gameHandler.getItemInfo(item.itemId);
|
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
|
|
|
|
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 {
|
2026-02-07 00:00:06 -08:00
|
|
|
|
itemName = "Item #" + std::to_string(item.itemId);
|
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-02-06 15:41:29 -08:00
|
|
|
|
ImVec4 qColor = InventoryScreen::getQualityColor(quality);
|
2026-03-17 21:17:22 -07:00
|
|
|
|
bool startsQuest = (info && info->startQuestId != 0);
|
2026-02-06 15:41:29 -08:00
|
|
|
|
|
|
|
|
|
|
// Get item icon
|
|
|
|
|
|
uint32_t displayId = item.displayInfoId;
|
|
|
|
|
|
if (displayId == 0 && info) displayId = info->displayInfoId;
|
2026-02-22 03:32:08 -08:00
|
|
|
|
VkDescriptorSet iconTex = inventoryScreen.getItemIcon(displayId);
|
2026-02-06 15:41:29 -08:00
|
|
|
|
|
|
|
|
|
|
ImVec2 cursor = ImGui::GetCursorScreenPos();
|
|
|
|
|
|
float rowH = std::max(iconSize, ImGui::GetTextLineHeight() * 2.0f);
|
|
|
|
|
|
|
|
|
|
|
|
// Invisible selectable for click handling
|
|
|
|
|
|
if (ImGui::Selectable("##loot", false, 0, ImVec2(0, rowH))) {
|
2026-03-11 21:27:16 -07:00
|
|
|
|
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);
|
|
|
|
|
|
size_t curLen = strlen(chatInputBuffer);
|
|
|
|
|
|
if (curLen + link.size() + 1 < sizeof(chatInputBuffer)) {
|
|
|
|
|
|
strncat(chatInputBuffer, link.c_str(), sizeof(chatInputBuffer) - curLen - 1);
|
|
|
|
|
|
chatInputMoveCursorToEnd = true;
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
lootSlotClicked = item.slotIndex;
|
|
|
|
|
|
}
|
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 18:34:45 -08:00
|
|
|
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
|
|
|
|
|
lootSlotClicked = item.slotIndex;
|
|
|
|
|
|
}
|
2026-02-06 15:41:29 -08:00
|
|
|
|
bool hovered = ImGui::IsItemHovered();
|
|
|
|
|
|
|
2026-03-10 20:53:21 -07:00
|
|
|
|
// Show item tooltip on hover
|
|
|
|
|
|
if (hovered && info && info->valid) {
|
|
|
|
|
|
inventoryScreen.renderItemTooltip(*info);
|
2026-03-13 11:11:33 -07:00
|
|
|
|
} 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-10 20:53:21 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 15:41:29 -08:00
|
|
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
|
|
|
|
|
|
|
|
|
|
// 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));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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));
|
|
|
|
|
|
}
|
2026-03-17 21:17:22 -07:00
|
|
|
|
// 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-02-06 15:41:29 -08: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-17 21:17:22 -07: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) {
|
2026-02-06 15:41:29 -08:00
|
|
|
|
char countStr[32];
|
|
|
|
|
|
snprintf(countStr, sizeof(countStr), "x%u", item.count);
|
2026-03-17 21:17:22 -07:00
|
|
|
|
drawList->AddText(ImVec2(textX, secondLineY), IM_COL32(200, 200, 200, 220), countStr);
|
2026-02-06 15:41:29 -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
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
// Process deferred loot pickup (after loop to avoid iterator invalidation)
|
|
|
|
|
|
if (lootSlotClicked >= 0) {
|
2026-03-12 17:58:24 -07:00
|
|
|
|
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));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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 ? dynamic_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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
if (loot.items.empty() && loot.gold == 0) {
|
2026-02-06 18:34:45 -08:00
|
|
|
|
gameHandler.closeLoot();
|
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-11 22:16:19 -07:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
if (ImGui::Button("Close", ImVec2(-1, 0))) {
|
|
|
|
|
|
gameHandler.closeLoot();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
gameHandler.closeLoot();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Gossip Window (Phase 5)
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderGossipWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isGossipWindowOpen()) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
|
2026-02-06 15:18:50 -08:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 150), 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
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
|
if (ImGui::Begin("NPC Dialog", &open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
|
|
|
|
const auto& gossip = gameHandler.getCurrentGossip();
|
|
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
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
|
|
|
|
// Gossip option icons - matches WoW GossipOptionIcon enum
|
|
|
|
|
|
static 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
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 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."},
|
|
|
|
|
|
};
|
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& opt : gossip.options) {
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(opt.id));
|
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
|
|
|
|
|
|
|
|
|
|
// Determine icon label - use text-based detection for shared icons
|
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 char* icon = (opt.icon < 11) ? gossipIcons[opt.icon] : "[Option]";
|
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 (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]";
|
|
|
|
|
|
|
|
|
|
|
|
// Resolve placeholder text from server
|
|
|
|
|
|
std::string displayText = opt.text;
|
|
|
|
|
|
auto placeholderIt = gossipPlaceholders.find(displayText);
|
|
|
|
|
|
if (placeholderIt != gossipPlaceholders.end()) {
|
|
|
|
|
|
displayText = placeholderIt->second;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string processedText = replaceGenderPlaceholders(displayText, gameHandler);
|
|
|
|
|
|
std::string label = std::string(icon) + " " + processedText;
|
|
|
|
|
|
if (ImGui::Selectable(label.c_str())) {
|
2026-03-10 16:21:09 -07:00
|
|
|
|
if (opt.text == "GOSSIP_OPTION_ARMORER") {
|
|
|
|
|
|
gameHandler.setVendorCanRepair(true);
|
|
|
|
|
|
}
|
2026-02-07 23:12:24 -08:00
|
|
|
|
gameHandler.selectGossipOption(opt.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
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 21:00:05 -08:00
|
|
|
|
// Fallback: some spirit healers don't send gossip options.
|
2026-02-07 23:12:24 -08:00
|
|
|
|
if (gossip.options.empty() && gameHandler.isPlayerGhost()) {
|
2026-02-07 21:00:05 -08:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
// Quest items
|
|
|
|
|
|
if (!gossip.quests.empty()) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Quests:");
|
2026-02-06 12:08:47 -08:00
|
|
|
|
for (size_t qi = 0; qi < gossip.quests.size(); qi++) {
|
|
|
|
|
|
const auto& quest = gossip.quests[qi];
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(qi));
|
2026-03-11 21:39:32 -07: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 = ImVec4(1.0f, 1.0f, 0.3f, 1.0f); // yellow
|
|
|
|
|
|
switch (quest.questIcon) {
|
|
|
|
|
|
case 5: // INCOMPLETE — in progress but not done
|
|
|
|
|
|
statusIcon = "?";
|
|
|
|
|
|
statusColor = ImVec4(0.65f, 0.65f, 0.65f, 1.0f); // gray
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 6: // REWARD_REP — repeatable, ready to turn in
|
|
|
|
|
|
case 10: // REWARD — ready to turn in
|
|
|
|
|
|
statusIcon = "?";
|
|
|
|
|
|
statusColor = ImVec4(1.0f, 1.0f, 0.3f, 1.0f); // yellow
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 7: // AVAILABLE_LOW — available but gray (low-level)
|
|
|
|
|
|
statusIcon = "!";
|
|
|
|
|
|
statusColor = ImVec4(0.65f, 0.65f, 0.65f, 1.0f); // gray
|
|
|
|
|
|
break;
|
|
|
|
|
|
default: // AVAILABLE (8) and any others
|
|
|
|
|
|
statusIcon = "!";
|
|
|
|
|
|
statusColor = ImVec4(1.0f, 1.0f, 0.3f, 1.0f); // yellow
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Render: colored icon glyph then [Lv] Title
|
|
|
|
|
|
ImGui::TextColored(statusColor, "%s", statusIcon);
|
|
|
|
|
|
ImGui::SameLine(0, 4);
|
2026-02-06 11:45:35 -08:00
|
|
|
|
char qlabel[256];
|
|
|
|
|
|
snprintf(qlabel, sizeof(qlabel), "[%d] %s", quest.questLevel, quest.title.c_str());
|
2026-03-11 21:39:32 -07:00
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, statusColor);
|
2026-02-06 11:45:35 -08:00
|
|
|
|
if (ImGui::Selectable(qlabel)) {
|
|
|
|
|
|
gameHandler.selectGossipQuest(quest.questId);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
ImGui::PopID();
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
if (ImGui::Button("Close", ImVec2(-1, 0))) {
|
|
|
|
|
|
gameHandler.closeGossip();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
gameHandler.closeGossip();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 11:59:51 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Quest Details Window
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderQuestDetailsWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isQuestDetailsOpen()) 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 / 2 - 225, screenH / 2 - 200), ImGuiCond_Appearing);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(450, 400), ImGuiCond_Appearing);
|
|
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
|
const auto& quest = gameHandler.getQuestDetails();
|
2026-02-09 17:39:21 -08:00
|
|
|
|
std::string processedTitle = replaceGenderPlaceholders(quest.title, gameHandler);
|
|
|
|
|
|
if (ImGui::Begin(processedTitle.c_str(), &open)) {
|
2026-02-06 11:59:51 -08:00
|
|
|
|
// Quest description
|
|
|
|
|
|
if (!quest.details.empty()) {
|
2026-02-09 17:39:21 -08:00
|
|
|
|
std::string processedDetails = replaceGenderPlaceholders(quest.details, gameHandler);
|
|
|
|
|
|
ImGui::TextWrapped("%s", processedDetails.c_str());
|
2026-02-06 11:59:51 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Objectives
|
|
|
|
|
|
if (!quest.objectives.empty()) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Objectives:");
|
2026-02-09 17:39:21 -08:00
|
|
|
|
std::string processedObjectives = replaceGenderPlaceholders(quest.objectives, gameHandler);
|
|
|
|
|
|
ImGui::TextWrapped("%s", processedObjectives.c_str());
|
2026-02-06 11:59:51 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 19:05:34 -07:00
|
|
|
|
// Choice reward items (player picks one)
|
2026-03-10 19:12:43 -07:00
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
std::string label;
|
|
|
|
|
|
ImVec4 nameCol = ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ri.count > 1) label += " x" + std::to_string(ri.count);
|
|
|
|
|
|
|
|
|
|
|
|
if (iconTex) {
|
|
|
|
|
|
ImGui::Image((void*)(intptr_t)iconTex, ImVec2(18, 18));
|
2026-03-11 21:11:58 -07:00
|
|
|
|
if (ImGui::IsItemHovered() && info && info->valid)
|
|
|
|
|
|
inventoryScreen.renderItemTooltip(*info);
|
2026-03-10 19:12:43 -07:00
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::TextColored(nameCol, " %s", label.c_str());
|
2026-03-11 21:11:58 -07:00
|
|
|
|
if (ImGui::IsItemHovered() && info && info->valid)
|
|
|
|
|
|
inventoryScreen.renderItemTooltip(*info);
|
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);
|
|
|
|
|
|
size_t curLen = strlen(chatInputBuffer);
|
|
|
|
|
|
if (curLen + link.size() + 1 < sizeof(chatInputBuffer)) {
|
|
|
|
|
|
strncat(chatInputBuffer, link.c_str(), sizeof(chatInputBuffer) - curLen - 1);
|
|
|
|
|
|
chatInputMoveCursorToEnd = true;
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-10 19:12:43 -07:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-10 19:05:34 -07:00
|
|
|
|
if (!quest.rewardChoiceItems.empty()) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Choose one reward:");
|
|
|
|
|
|
for (const auto& ri : quest.rewardChoiceItems) {
|
2026-03-10 19:12:43 -07:00
|
|
|
|
renderQuestRewardItem(ri);
|
2026-03-10 19:05:34 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Fixed reward items (always given)
|
|
|
|
|
|
if (!quest.rewardItems.empty()) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "You will receive:");
|
|
|
|
|
|
for (const auto& ri : quest.rewardItems) {
|
2026-03-10 19:12:43 -07:00
|
|
|
|
renderQuestRewardItem(ri);
|
2026-03-10 19:05:34 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// XP and money rewards
|
2026-02-06 11:59:51 -08:00
|
|
|
|
if (quest.rewardXp > 0 || quest.rewardMoney > 0) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Rewards:");
|
|
|
|
|
|
if (quest.rewardXp > 0) {
|
|
|
|
|
|
ImGui::Text(" %u experience", quest.rewardXp);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (quest.rewardMoney > 0) {
|
|
|
|
|
|
uint32_t gold = quest.rewardMoney / 10000;
|
|
|
|
|
|
uint32_t silver = (quest.rewardMoney % 10000) / 100;
|
|
|
|
|
|
uint32_t copper = quest.rewardMoney % 100;
|
2026-03-12 08:15:46 -07:00
|
|
|
|
ImGui::TextDisabled(" Money:"); ImGui::SameLine(0, 4);
|
|
|
|
|
|
renderCoinsText(gold, silver, copper);
|
2026-02-06 11:59:51 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (quest.suggestedPlayers > 1) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f),
|
|
|
|
|
|
"Suggested players: %u", quest.suggestedPlayers);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
gameHandler.declineQuest();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 21:50:15 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Quest Request Items Window (turn-in progress check)
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderQuestRequestItemsWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isQuestRequestItemsOpen()) 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 / 2 - 225, screenH / 2 - 200), ImGuiCond_Appearing);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(450, 350), ImGuiCond_Appearing);
|
|
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
|
const auto& quest = gameHandler.getQuestRequestItems();
|
2026-02-19 00:56:24 -08:00
|
|
|
|
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-02-09 17:39:21 -08:00
|
|
|
|
std::string processedTitle = replaceGenderPlaceholders(quest.title, gameHandler);
|
|
|
|
|
|
if (ImGui::Begin(processedTitle.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
|
2026-02-06 21:50:15 -08:00
|
|
|
|
if (!quest.completionText.empty()) {
|
2026-02-09 17:39:21 -08:00
|
|
|
|
std::string processedCompletionText = replaceGenderPlaceholders(quest.completionText, gameHandler);
|
|
|
|
|
|
ImGui::TextWrapped("%s", processedCompletionText.c_str());
|
2026-02-06 21:50:15 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Required items
|
|
|
|
|
|
if (!quest.requiredItems.empty()) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Required Items:");
|
|
|
|
|
|
for (const auto& item : quest.requiredItems) {
|
2026-02-19 00:56:24 -08:00
|
|
|
|
uint32_t have = countItemInInventory(item.itemId);
|
|
|
|
|
|
bool enough = have >= item.count;
|
2026-03-11 20:41:02 -07:00
|
|
|
|
ImVec4 textCol = enough ? ImVec4(0.6f, 1.0f, 0.6f, 1.0f) : ImVec4(1.0f, 0.6f, 0.6f, 1.0f);
|
2026-02-06 21:50:15 -08:00
|
|
|
|
auto* info = gameHandler.getItemInfo(item.itemId);
|
2026-02-19 00:56:24 -08:00
|
|
|
|
const char* name = (info && info->valid) ? info->name.c_str() : nullptr;
|
2026-03-11 20:41:02 -07: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-02-19 00:56:24 -08:00
|
|
|
|
if (name && *name) {
|
2026-03-11 20:41:02 -07:00
|
|
|
|
ImGui::TextColored(textCol, "%s %u/%u", name, have, item.count);
|
2026-02-19 00:56:24 -08:00
|
|
|
|
} else {
|
2026-03-11 20:41:02 -07:00
|
|
|
|
ImGui::TextColored(textCol, "Item %u %u/%u", item.itemId, have, item.count);
|
2026-02-19 00:56:24 -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);
|
|
|
|
|
|
size_t curLen = strlen(chatInputBuffer);
|
|
|
|
|
|
if (curLen + link.size() + 1 < sizeof(chatInputBuffer)) {
|
|
|
|
|
|
strncat(chatInputBuffer, link.c_str(), sizeof(chatInputBuffer) - curLen - 1);
|
|
|
|
|
|
chatInputMoveCursorToEnd = true;
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-06 21:50:15 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (quest.requiredMoney > 0) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
uint32_t g = quest.requiredMoney / 10000;
|
|
|
|
|
|
uint32_t s = (quest.requiredMoney % 10000) / 100;
|
|
|
|
|
|
uint32_t c = quest.requiredMoney % 100;
|
2026-03-12 08:15:46 -07:00
|
|
|
|
ImGui::TextDisabled("Required money:"); ImGui::SameLine(0, 4);
|
|
|
|
|
|
renderCoinsText(g, s, c);
|
2026-02-06 21:50:15 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Complete / Cancel buttons
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
float buttonW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
2026-02-19 00:56:24 -08:00
|
|
|
|
if (ImGui::Button("Complete Quest", ImVec2(buttonW, 0))) {
|
|
|
|
|
|
gameHandler.completeQuest();
|
2026-02-06 21:50:15 -08:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(buttonW, 0))) {
|
|
|
|
|
|
gameHandler.closeQuestRequestItems();
|
|
|
|
|
|
}
|
2026-02-19 00:56:24 -08:00
|
|
|
|
|
|
|
|
|
|
if (!quest.isCompletable()) {
|
|
|
|
|
|
ImGui::TextDisabled("Server flagged this quest as incomplete; completion will be server-validated.");
|
|
|
|
|
|
}
|
2026-02-06 21:50:15 -08:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
gameHandler.closeQuestRequestItems();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Quest Offer Reward Window (choose reward)
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderQuestOfferRewardWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isQuestOfferRewardOpen()) 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 / 2 - 225, screenH / 2 - 200), ImGuiCond_Appearing);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(450, 400), ImGuiCond_Appearing);
|
|
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
|
const auto& quest = gameHandler.getQuestOfferReward();
|
|
|
|
|
|
static int selectedChoice = -1;
|
|
|
|
|
|
|
2026-02-10 01:24:37 -08:00
|
|
|
|
// Auto-select if only one choice reward
|
|
|
|
|
|
if (quest.choiceRewards.size() == 1 && selectedChoice == -1) {
|
|
|
|
|
|
selectedChoice = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 17:39:21 -08:00
|
|
|
|
std::string processedTitle = replaceGenderPlaceholders(quest.title, gameHandler);
|
|
|
|
|
|
if (ImGui::Begin(processedTitle.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
|
2026-02-06 21:50:15 -08:00
|
|
|
|
if (!quest.rewardText.empty()) {
|
2026-02-09 17:39:21 -08:00
|
|
|
|
std::string processedRewardText = replaceGenderPlaceholders(quest.rewardText, gameHandler);
|
|
|
|
|
|
ImGui::TextWrapped("%s", processedRewardText.c_str());
|
2026-02-06 21:50:15 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Choice rewards (pick one)
|
2026-03-10 19:25:26 -07:00
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
|
|
|
|
// 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))
|
|
|
|
|
|
: ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
|
|
|
|
|
return {iconTex, col};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-11 01:00:08 -07:00
|
|
|
|
// Helper: show full item tooltip (reuses InventoryScreen's rich tooltip)
|
|
|
|
|
|
auto rewardItemTooltip = [&](const game::QuestRewardItem& ri, ImVec4 /*nameCol*/) {
|
2026-03-10 19:25:26 -07:00
|
|
|
|
auto* info = gameHandler.getItemInfo(ri.itemId);
|
2026-03-11 01:00:08 -07:00
|
|
|
|
if (!info || !info->valid) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
ImGui::TextDisabled("Loading item data...");
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
inventoryScreen.renderItemTooltip(*info);
|
2026-03-10 19:25:26 -07:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-06 21:50:15 -08:00
|
|
|
|
if (!quest.choiceRewards.empty()) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Choose a reward:");
|
2026-02-10 01:24:37 -08:00
|
|
|
|
|
2026-02-06 21:50:15 -08:00
|
|
|
|
for (size_t i = 0; i < quest.choiceRewards.size(); ++i) {
|
|
|
|
|
|
const auto& item = quest.choiceRewards[i];
|
|
|
|
|
|
auto* info = gameHandler.getItemInfo(item.itemId);
|
2026-03-10 19:25:26 -07:00
|
|
|
|
auto [iconTex, qualityColor] = resolveRewardItemVis(item);
|
2026-02-06 21:50:15 -08:00
|
|
|
|
|
2026-03-10 19:25:26 -07: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-10 01:24:37 -08:00
|
|
|
|
|
2026-03-10 19:25:26 -07:00
|
|
|
|
bool selected = (selectedChoice == static_cast<int>(i));
|
2026-02-10 01:24:37 -08:00
|
|
|
|
ImGui::PushID(static_cast<int>(i));
|
2026-03-10 19:25:26 -07:00
|
|
|
|
|
|
|
|
|
|
// Icon then selectable on same line
|
2026-02-10 01:24:37 -08:00
|
|
|
|
if (iconTex) {
|
2026-03-10 19:25:26 -07:00
|
|
|
|
ImGui::Image((void*)(intptr_t)iconTex, ImVec2(20, 20));
|
|
|
|
|
|
if (ImGui::IsItemHovered()) rewardItemTooltip(item, qualityColor);
|
2026-02-10 01:24:37 -08:00
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
}
|
2026-03-10 19:25:26 -07:00
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, qualityColor);
|
|
|
|
|
|
if (ImGui::Selectable(label.c_str(), selected, 0, ImVec2(0, 20))) {
|
2026-03-11 21:31:09 -07:00
|
|
|
|
if (ImGui::GetIO().KeyShift && info && info->valid && !info->name.empty()) {
|
|
|
|
|
|
std::string link = buildItemChatLink(info->entry, info->quality, info->name);
|
|
|
|
|
|
size_t curLen = strlen(chatInputBuffer);
|
|
|
|
|
|
if (curLen + link.size() + 1 < sizeof(chatInputBuffer)) {
|
|
|
|
|
|
strncat(chatInputBuffer, link.c_str(), sizeof(chatInputBuffer) - curLen - 1);
|
|
|
|
|
|
chatInputMoveCursorToEnd = true;
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
selectedChoice = static_cast<int>(i);
|
|
|
|
|
|
}
|
2026-03-10 19:25:26 -07:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
if (ImGui::IsItemHovered()) rewardItemTooltip(item, qualityColor);
|
|
|
|
|
|
|
2026-02-10 01:24:37 -08:00
|
|
|
|
ImGui::PopID();
|
2026-02-06 21:50:15 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Fixed rewards (always given)
|
|
|
|
|
|
if (!quest.fixedRewards.empty()) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "You will also receive:");
|
|
|
|
|
|
for (const auto& item : quest.fixedRewards) {
|
|
|
|
|
|
auto* info = gameHandler.getItemInfo(item.itemId);
|
2026-03-10 19:25:26 -07:00
|
|
|
|
auto [iconTex, qualityColor] = resolveRewardItemVis(item);
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
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);
|
|
|
|
|
|
size_t curLen = strlen(chatInputBuffer);
|
|
|
|
|
|
if (curLen + link.size() + 1 < sizeof(chatInputBuffer)) {
|
|
|
|
|
|
strncat(chatInputBuffer, link.c_str(), sizeof(chatInputBuffer) - curLen - 1);
|
|
|
|
|
|
chatInputMoveCursorToEnd = true;
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-06 21:50:15 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Money / XP rewards
|
|
|
|
|
|
if (quest.rewardXp > 0 || quest.rewardMoney > 0) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Rewards:");
|
|
|
|
|
|
if (quest.rewardXp > 0)
|
|
|
|
|
|
ImGui::Text(" %u experience", quest.rewardXp);
|
|
|
|
|
|
if (quest.rewardMoney > 0) {
|
|
|
|
|
|
uint32_t g = quest.rewardMoney / 10000;
|
|
|
|
|
|
uint32_t s = (quest.rewardMoney % 10000) / 100;
|
|
|
|
|
|
uint32_t c = quest.rewardMoney % 100;
|
2026-03-12 08:15:46 -07:00
|
|
|
|
ImGui::TextDisabled(" Money:"); ImGui::SameLine(0, 4);
|
|
|
|
|
|
renderCoinsText(g, s, c);
|
2026-02-06 21:50:15 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Complete button
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
float buttonW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
|
|
|
|
|
|
|
|
|
|
|
bool canComplete = quest.choiceRewards.empty() || selectedChoice >= 0;
|
|
|
|
|
|
if (!canComplete) ImGui::BeginDisabled();
|
|
|
|
|
|
if (ImGui::Button("Complete Quest", ImVec2(buttonW, 0))) {
|
2026-02-19 03:12:57 -08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-02-06 21:50:15 -08:00
|
|
|
|
gameHandler.chooseQuestReward(rewardIdx);
|
|
|
|
|
|
selectedChoice = -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!canComplete) ImGui::EndDisabled();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(buttonW, 0))) {
|
|
|
|
|
|
gameHandler.closeQuestOfferReward();
|
|
|
|
|
|
selectedChoice = -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
gameHandler.closeQuestOfferReward();
|
|
|
|
|
|
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
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Vendor Window (Phase 5)
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isVendorWindowOpen()) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
|
2026-02-06 15:18:50 -08: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;
|
|
|
|
|
|
if (ImGui::Begin("Vendor", &open)) {
|
|
|
|
|
|
const auto& vendor = gameHandler.getVendorItems();
|
|
|
|
|
|
|
2026-02-06 11:59:51 -08:00
|
|
|
|
// Show player money
|
|
|
|
|
|
uint64_t money = gameHandler.getMoneyCopper();
|
|
|
|
|
|
uint32_t mg = static_cast<uint32_t>(money / 10000);
|
|
|
|
|
|
uint32_t ms = static_cast<uint32_t>((money / 100) % 100);
|
|
|
|
|
|
uint32_t mc = static_cast<uint32_t>(money % 100);
|
2026-03-12 08:10:17 -07:00
|
|
|
|
ImGui::TextDisabled("Your money:"); ImGui::SameLine(0, 4);
|
|
|
|
|
|
renderCoinsText(mg, ms, mc);
|
2026-03-10 16:21:09 -07:00
|
|
|
|
|
|
|
|
|
|
if (vendor.canRepair) {
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 8.0f);
|
|
|
|
|
|
if (ImGui::SmallButton("Repair All")) {
|
|
|
|
|
|
gameHandler.repairAll(vendor.vendorGuid, false);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
2026-03-13 10:29:56 -07:00
|
|
|
|
ImGui::SetTooltip("Repair all equipped items using your gold");
|
|
|
|
|
|
}
|
|
|
|
|
|
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 16:21:09 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-06 11:59:51 -08:00
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
2026-02-06 15:41:29 -08:00
|
|
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Right-click bag items to sell");
|
2026-03-11 22:44:23 -07: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;
|
|
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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-02-06 15:41:29 -08:00
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
2026-02-19 05:48:40 -08:00
|
|
|
|
const auto& buyback = gameHandler.getBuybackItems();
|
|
|
|
|
|
if (!buyback.empty()) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Buy Back");
|
2026-03-11 20:52:42 -07:00
|
|
|
|
if (ImGui::BeginTable("BuybackTable", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
|
|
|
|
|
|
ImGui::TableSetupColumn("##icon", ImGuiTableColumnFlags_WidthFixed, 22.0f);
|
2026-02-19 05:48:40 -08:00
|
|
|
|
ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch);
|
|
|
|
|
|
ImGui::TableSetupColumn("Price", ImGuiTableColumnFlags_WidthFixed, 110.0f);
|
|
|
|
|
|
ImGui::TableSetupColumn("Buy", ImGuiTableColumnFlags_WidthFixed, 62.0f);
|
|
|
|
|
|
ImGui::TableHeadersRow();
|
2026-03-11 23:08:35 -07:00
|
|
|
|
// 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;
|
2026-03-11 20:52:42 -07:00
|
|
|
|
}
|
2026-03-11 23:08:35 -07:00
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
2026-03-12 08:15:46 -07:00
|
|
|
|
if (canAfford) {
|
|
|
|
|
|
renderCoinsText(g, s, c);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%ug %us %uc", g, s, c);
|
|
|
|
|
|
}
|
2026-03-11 23:08:35 -07:00
|
|
|
|
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 20:52:42 -07:00
|
|
|
|
}
|
2026-02-19 05:48:40 -08:00
|
|
|
|
ImGui::EndTable();
|
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
if (vendor.items.empty()) {
|
|
|
|
|
|
ImGui::TextDisabled("This vendor has nothing for sale.");
|
|
|
|
|
|
} else {
|
2026-03-11 22:10:43 -07:00
|
|
|
|
// Search + quantity controls on one row
|
|
|
|
|
|
ImGui::SetNextItemWidth(200.0f);
|
2026-03-11 21:19:47 -07:00
|
|
|
|
ImGui::InputTextWithHint("##VendorSearch", "Search...", vendorSearchFilter_, sizeof(vendorSearchFilter_));
|
2026-03-11 22:10:43 -07:00
|
|
|
|
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;
|
2026-03-11 21:19:47 -07:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
2026-03-11 20:49:25 -07:00
|
|
|
|
if (ImGui::BeginTable("VendorTable", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) {
|
|
|
|
|
|
ImGui::TableSetupColumn("##icon", ImGuiTableColumnFlags_WidthFixed, 22.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::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();
|
|
|
|
|
|
|
2026-03-11 21:19:47 -07: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)));
|
|
|
|
|
|
|
2026-02-17 18:55:02 -08: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-11 19:29:10 -07:00
|
|
|
|
// Proactively ensure vendor item info is loaded
|
|
|
|
|
|
gameHandler.ensureItemInfo(item.itemId);
|
2026-03-11 20:49:25 -07:00
|
|
|
|
auto* info = gameHandler.getItemInfo(item.itemId);
|
2026-03-11 19:29:10 -07:00
|
|
|
|
|
2026-03-11 21:19:47 -07: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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
|
|
ImGui::PushID(vi);
|
|
|
|
|
|
|
2026-03-11 20:49:25 -07:00
|
|
|
|
// Icon column
|
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::TableSetColumnIndex(0);
|
2026-03-11 20:49:25 -07:00
|
|
|
|
{
|
|
|
|
|
|
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));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Name column
|
|
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
2026-02-06 11:59:51 -08:00
|
|
|
|
if (info && info->valid) {
|
2026-03-11 20:49:25 -07:00
|
|
|
|
ImVec4 qc = InventoryScreen::getQualityColor(static_cast<game::ItemQuality>(info->quality));
|
|
|
|
|
|
ImGui::TextColored(qc, "%s", info->name.c_str());
|
2026-02-06 11:59:51 -08:00
|
|
|
|
if (ImGui::IsItemHovered()) {
|
2026-03-12 05:20:44 -07:00
|
|
|
|
inventoryScreen.renderItemTooltip(*info, &gameHandler.getInventory());
|
2026-02-06 11:59:51 -08:00
|
|
|
|
}
|
2026-03-11 21:27:16 -07:00
|
|
|
|
// Shift-click: insert item link into chat
|
|
|
|
|
|
if (ImGui::IsItemClicked() && ImGui::GetIO().KeyShift) {
|
|
|
|
|
|
std::string link = buildItemChatLink(info->entry, info->quality, info->name);
|
|
|
|
|
|
size_t curLen = strlen(chatInputBuffer);
|
|
|
|
|
|
if (curLen + link.size() + 1 < sizeof(chatInputBuffer)) {
|
|
|
|
|
|
strncat(chatInputBuffer, link.c_str(), sizeof(chatInputBuffer) - curLen - 1);
|
|
|
|
|
|
chatInputMoveCursorToEnd = true;
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-06 11:59:51 -08:00
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::Text("Item %u", 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-11 20:49:25 -07:00
|
|
|
|
ImGui::TableSetColumnIndex(2);
|
2026-02-17 18:08:00 -08:00
|
|
|
|
if (item.buyPrice == 0 && item.extendedCost != 0) {
|
|
|
|
|
|
// Token-only item (no gold cost)
|
2026-02-17 17:44:48 -08:00
|
|
|
|
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "[Tokens]");
|
|
|
|
|
|
} 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;
|
2026-03-12 08:10:17 -07:00
|
|
|
|
if (canAfford) {
|
|
|
|
|
|
renderCoinsText(g, s, c);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%ug %us %uc", g, s, c);
|
|
|
|
|
|
}
|
2026-02-17 17:44:48 -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
|
|
|
|
|
2026-03-11 20:49:25 -07:00
|
|
|
|
ImGui::TableSetColumnIndex(3);
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
if (item.maxCount < 0) {
|
2026-03-11 22:10:43 -07:00
|
|
|
|
ImGui::TextDisabled("Inf");
|
|
|
|
|
|
} else if (item.maxCount == 0) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Out");
|
|
|
|
|
|
} else if (item.maxCount <= 5) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.1f, 1.0f), "%d", item.maxCount);
|
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 {
|
|
|
|
|
|
ImGui::Text("%d", item.maxCount);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 20:49:25 -07:00
|
|
|
|
ImGui::TableSetColumnIndex(4);
|
2026-03-11 22:10:43 -07:00
|
|
|
|
bool outOfStock = (item.maxCount == 0);
|
|
|
|
|
|
if (outOfStock) ImGui::BeginDisabled();
|
2026-02-19 05:48:40 -08:00
|
|
|
|
std::string buyBtnId = "Buy##vendor_" + std::to_string(vi);
|
|
|
|
|
|
if (ImGui::SmallButton(buyBtnId.c_str())) {
|
2026-03-11 22:10:43 -07:00
|
|
|
|
int qty = vendorBuyQty;
|
|
|
|
|
|
if (item.maxCount > 0 && qty > item.maxCount) qty = item.maxCount;
|
|
|
|
|
|
gameHandler.buyItem(vendor.vendorGuid, item.itemId, item.slot,
|
|
|
|
|
|
static_cast<uint32_t>(qty));
|
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 22:10:43 -07:00
|
|
|
|
if (outOfStock) ImGui::EndDisabled();
|
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::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::EndTable();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
2026-02-06 11:59:51 -08:00
|
|
|
|
gameHandler.closeVendor();
|
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-08 14:33:39 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Trainer
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isTrainerWindowOpen()) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
2026-03-11 20:48:03 -07:00
|
|
|
|
auto* assetMgr = core::Application::getInstance().getAssetManager();
|
2026-02-08 14:33:39 -08:00
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 225, 100), ImGuiCond_Appearing);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(500, 450), ImGuiCond_Appearing);
|
|
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
|
if (ImGui::Begin("Trainer", &open)) {
|
2026-03-14 07:43:52 -07:00
|
|
|
|
// If user clicked window close, short-circuit before rendering large trainer tables.
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
gameHandler.closeTrainer();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 14:33:39 -08:00
|
|
|
|
const auto& trainer = gameHandler.getTrainerSpells();
|
2026-03-17 10:12:49 -07:00
|
|
|
|
const bool isProfessionTrainer = (trainer.trainerType == 2);
|
2026-02-08 14:33:39 -08: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());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Greeting
|
|
|
|
|
|
if (!trainer.greeting.empty()) {
|
|
|
|
|
|
ImGui::TextWrapped("%s", trainer.greeting.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
// Player money
|
|
|
|
|
|
uint64_t money = gameHandler.getMoneyCopper();
|
|
|
|
|
|
uint32_t mg = static_cast<uint32_t>(money / 10000);
|
|
|
|
|
|
uint32_t ms = static_cast<uint32_t>((money / 100) % 100);
|
|
|
|
|
|
uint32_t mc = static_cast<uint32_t>(money % 100);
|
2026-03-12 08:10:17 -07:00
|
|
|
|
ImGui::TextDisabled("Your money:"); ImGui::SameLine(0, 4);
|
|
|
|
|
|
renderCoinsText(mg, ms, mc);
|
2026-02-10 01:24:37 -08:00
|
|
|
|
|
2026-03-11 21:21:14 -07:00
|
|
|
|
// Filter controls
|
2026-02-10 01:24:37 -08:00
|
|
|
|
static bool showUnavailable = false;
|
|
|
|
|
|
ImGui::Checkbox("Show unavailable spells", &showUnavailable);
|
2026-03-11 21:21:14 -07:00
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::SetNextItemWidth(-1.0f);
|
|
|
|
|
|
ImGui::InputTextWithHint("##TrainerSearch", "Search...", trainerSearchFilter_, sizeof(trainerSearchFilter_));
|
2026-02-08 14:33:39 -08:00
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
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) {
|
2026-02-08 15:03:43 -08:00
|
|
|
|
if (id == 0) return true;
|
2026-02-10 01:24:37 -08:00
|
|
|
|
// Check if spell is in knownSpells list
|
2026-02-17 15:13:54 -08:00
|
|
|
|
bool found = knownSpells.count(id);
|
2026-02-10 01:24:37 -08:00
|
|
|
|
if (found) return true;
|
|
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
2026-02-09 22:13:31 -08:00
|
|
|
|
}
|
2026-02-10 01:24:37 -08:00
|
|
|
|
return false;
|
2026-02-08 14:33:39 -08:00
|
|
|
|
};
|
2026-02-08 15:03:43 -08:00
|
|
|
|
uint32_t playerLevel = gameHandler.getPlayerLevel();
|
2026-02-08 14:33:39 -08:00
|
|
|
|
|
2026-02-08 14:46:01 -08:00
|
|
|
|
// Renders spell rows into the current table
|
|
|
|
|
|
auto renderSpellRows = [&](const std::vector<const game::TrainerSpell*>& spells) {
|
|
|
|
|
|
for (const auto* spell : spells) {
|
2026-02-10 01:24:37 -08:00
|
|
|
|
// Check prerequisites client-side first
|
2026-02-09 22:13:31 -08:00
|
|
|
|
bool prereq1Met = isKnown(spell->chainNode1);
|
|
|
|
|
|
bool prereq2Met = isKnown(spell->chainNode2);
|
|
|
|
|
|
bool prereq3Met = isKnown(spell->chainNode3);
|
|
|
|
|
|
bool prereqsMet = prereq1Met && prereq2Met && prereq3Met;
|
2026-02-08 15:03:43 -08:00
|
|
|
|
bool levelMet = (spell->reqLevel == 0 || playerLevel >= spell->reqLevel);
|
2026-02-09 22:13:31 -08:00
|
|
|
|
bool alreadyKnown = isKnown(spell->spellId);
|
2026-02-08 15:03:43 -08:00
|
|
|
|
|
2026-02-10 01:24:37 -08: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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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-03-11 21:21:14 -07: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-10 01:24:37 -08:00
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(spell->spellId));
|
|
|
|
|
|
|
2026-02-08 14:33:39 -08:00
|
|
|
|
ImVec4 color;
|
|
|
|
|
|
const char* statusLabel;
|
2026-02-10 01:24:37 -08:00
|
|
|
|
// WotLK trainer states: 0=available, 1=unavailable, 2=known
|
|
|
|
|
|
if (effectiveState == 2 || alreadyKnown) {
|
2026-02-08 14:33:39 -08:00
|
|
|
|
color = ImVec4(0.3f, 0.9f, 0.3f, 1.0f);
|
|
|
|
|
|
statusLabel = "Known";
|
2026-02-10 01:24:37 -08:00
|
|
|
|
} else if (effectiveState == 0) {
|
2026-02-08 14:33:39 -08:00
|
|
|
|
color = ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
|
|
|
|
|
statusLabel = "Available";
|
|
|
|
|
|
} else {
|
|
|
|
|
|
color = ImVec4(0.6f, 0.3f, 0.3f, 1.0f);
|
|
|
|
|
|
statusLabel = "Unavailable";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 20:48:03 -07:00
|
|
|
|
// Icon column
|
2026-02-08 14:33:39 -08:00
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
2026-03-11 20:48:03 -07:00
|
|
|
|
{
|
|
|
|
|
|
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));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Spell name
|
|
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
2026-02-08 14:46:01 -08:00
|
|
|
|
const std::string& name = gameHandler.getSpellName(spell->spellId);
|
|
|
|
|
|
const std::string& rank = gameHandler.getSpellRank(spell->spellId);
|
2026-02-08 14:33:39 -08:00
|
|
|
|
if (!name.empty()) {
|
2026-02-08 14:46:01 -08:00
|
|
|
|
if (!rank.empty())
|
2026-02-08 14:33:39 -08:00
|
|
|
|
ImGui::TextColored(color, "%s (%s)", name.c_str(), rank.c_str());
|
2026-02-08 14:46:01 -08:00
|
|
|
|
else
|
2026-02-08 14:33:39 -08:00
|
|
|
|
ImGui::TextColored(color, "%s", name.c_str());
|
|
|
|
|
|
} else {
|
2026-02-08 14:46:01 -08:00
|
|
|
|
ImGui::TextColored(color, "Spell #%u", spell->spellId);
|
2026-02-08 14:33:39 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
if (!name.empty()) {
|
2026-03-12 13:09:40 -07:00
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "%s", name.c_str());
|
|
|
|
|
|
if (!rank.empty()) ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "%s", rank.c_str());
|
2026-02-08 14:33:39 -08:00
|
|
|
|
}
|
2026-03-12 13:09:40 -07:00
|
|
|
|
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);
|
2026-02-08 15:03:43 -08:00
|
|
|
|
if (spell->reqLevel > 0) {
|
|
|
|
|
|
ImVec4 lvlColor = levelMet ? ImVec4(0.7f, 0.7f, 0.7f, 1.0f) : ImVec4(1.0f, 0.3f, 0.3f, 1.0f);
|
|
|
|
|
|
ImGui::TextColored(lvlColor, "Required Level: %u", spell->reqLevel);
|
2026-02-08 14:33:39 -08:00
|
|
|
|
}
|
2026-02-08 15:03:43 -08:00
|
|
|
|
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 ? ImVec4(0.3f, 0.9f, 0.3f, 1.0f) : ImVec4(1.0f, 0.3f, 0.3f, 1.0f);
|
|
|
|
|
|
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);
|
2026-02-08 14:33:39 -08:00
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Level
|
2026-03-11 20:48:03 -07:00
|
|
|
|
ImGui::TableSetColumnIndex(2);
|
2026-02-08 14:46:01 -08:00
|
|
|
|
ImGui::TextColored(color, "%u", spell->reqLevel);
|
2026-02-08 14:33:39 -08:00
|
|
|
|
|
|
|
|
|
|
// Cost
|
2026-03-11 20:48:03 -07:00
|
|
|
|
ImGui::TableSetColumnIndex(3);
|
2026-02-08 14:46:01 -08:00
|
|
|
|
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;
|
2026-03-12 08:10:17 -07:00
|
|
|
|
if (canAfford) {
|
|
|
|
|
|
renderCoinsText(g, s, c);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%ug %us %uc", g, s, c);
|
|
|
|
|
|
}
|
2026-02-08 14:33:39 -08:00
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(color, "Free");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 15:03:43 -08:00
|
|
|
|
// Train button - only enabled if available, affordable, prereqs met
|
2026-03-11 20:48:03 -07:00
|
|
|
|
ImGui::TableSetColumnIndex(4);
|
2026-02-10 01:24:37 -08:00
|
|
|
|
// Use effectiveState so newly available spells (after learning prereqs) can be trained
|
|
|
|
|
|
bool canTrain = !alreadyKnown && effectiveState == 0
|
2026-02-08 15:03:43 -08:00
|
|
|
|
&& prereqsMet && levelMet
|
|
|
|
|
|
&& (money >= spell->spellCost);
|
2026-02-09 21:59:00 -08:00
|
|
|
|
|
2026-02-09 22:13:31 -08: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) {
|
2026-02-09 21:59:00 -08:00
|
|
|
|
LOG_INFO("Trainer button debug: spellId=", spell->spellId,
|
|
|
|
|
|
" alreadyKnown=", alreadyKnown, " state=", (int)spell->state,
|
2026-02-09 22:13:31 -08:00
|
|
|
|
" prereqsMet=", prereqsMet, " (", prereq1Met, ",", prereq2Met, ",", prereq3Met, ")",
|
|
|
|
|
|
" levelMet=", levelMet,
|
|
|
|
|
|
" reqLevel=", spell->reqLevel, " playerLevel=", playerLevel,
|
|
|
|
|
|
" chain1=", spell->chainNode1, " chain2=", spell->chainNode2, " chain3=", spell->chainNode3,
|
2026-02-09 21:59:00 -08:00
|
|
|
|
" canAfford=", (money >= spell->spellCost),
|
|
|
|
|
|
" canTrain=", canTrain);
|
2026-02-09 22:13:31 -08:00
|
|
|
|
logCount++;
|
2026-02-09 21:59:00 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 10:12:49 -07: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-08 14:33:39 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
2026-02-08 14:46:01 -08:00
|
|
|
|
};
|
2026-02-08 14:33:39 -08:00
|
|
|
|
|
2026-02-08 14:46:01 -08:00
|
|
|
|
auto renderSpellTable = [&](const char* tableId, const std::vector<const game::TrainerSpell*>& spells) {
|
2026-03-11 20:48:03 -07:00
|
|
|
|
if (ImGui::BeginTable(tableId, 5,
|
2026-02-08 14:46:01 -08:00
|
|
|
|
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) {
|
2026-03-11 20:48:03 -07:00
|
|
|
|
ImGui::TableSetupColumn("##icon", ImGuiTableColumnFlags_WidthFixed, 22.0f);
|
2026-02-08 14:46:01 -08:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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());
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
2026-02-18 20:10:47 -08:00
|
|
|
|
allSpells.reserve(trainer.spells.size());
|
2026-02-08 14:46:01 -08:00
|
|
|
|
for (const auto& spell : trainer.spells) {
|
|
|
|
|
|
allSpells.push_back(&spell);
|
|
|
|
|
|
}
|
|
|
|
|
|
renderSpellTable("TrainerTable", allSpells);
|
2026-02-08 14:33:39 -08:00
|
|
|
|
}
|
2026-03-11 22:23:26 -07: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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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-03-17 10:12:49 -07:00
|
|
|
|
|
|
|
|
|
|
// Profession trainer: craft quantity controls
|
|
|
|
|
|
if (isProfessionTrainer) {
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
static int craftQuantity = 1;
|
|
|
|
|
|
static uint32_t selectedCraftSpell = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!canCraft) ImGui::EndDisabled();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-08 14:33:39 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
gameHandler.closeTrainer();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 18:27:52 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Teleporter Panel
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
2026-02-05 16:01:38 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Escape Menu
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderEscapeMenu() {
|
|
|
|
|
|
if (!showEscapeMenu) return;
|
|
|
|
|
|
|
|
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
|
|
float screenW = io.DisplaySize.x;
|
|
|
|
|
|
float screenH = io.DisplaySize.y;
|
2026-03-12 02:31:12 -07:00
|
|
|
|
ImVec2 size(260.0f, 248.0f);
|
2026-02-05 16:01:38 -08:00
|
|
|
|
ImVec2 pos((screenW - size.x) * 0.5f, (screenH - size.y) * 0.5f);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(pos, ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(size, ImGuiCond_Always);
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar;
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
showEscapeSettingsNotice = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::Button("Quit", ImVec2(-1, 0))) {
|
2026-02-05 16:17:04 -08:00
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
if (auto* music = renderer->getMusicManager()) {
|
|
|
|
|
|
music->stopMusic(0.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-05 16:01:38 -08:00
|
|
|
|
core::Application::getInstance().shutdown();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::Button("Settings", ImVec2(-1, 0))) {
|
2026-02-05 16:11:00 -08:00
|
|
|
|
showEscapeSettingsNotice = false;
|
|
|
|
|
|
showSettingsWindow = true;
|
|
|
|
|
|
settingsInit = false;
|
2026-02-09 17:39:21 -08:00
|
|
|
|
showEscapeMenu = false;
|
2026-02-05 16:01:38 -08:00
|
|
|
|
}
|
2026-03-09 15:52:58 -07:00
|
|
|
|
if (ImGui::Button("Instance Lockouts", ImVec2(-1, 0))) {
|
|
|
|
|
|
showInstanceLockouts_ = true;
|
|
|
|
|
|
showEscapeMenu = false;
|
|
|
|
|
|
}
|
2026-03-12 02:31:12 -07:00
|
|
|
|
if (ImGui::Button("Help / GM Ticket", ImVec2(-1, 0))) {
|
|
|
|
|
|
showGmTicketWindow_ = true;
|
|
|
|
|
|
showEscapeMenu = false;
|
|
|
|
|
|
}
|
2026-02-05 16:01:38 -08:00
|
|
|
|
|
2026-02-05 16:21:17 -08:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 10.0f));
|
|
|
|
|
|
if (ImGui::Button("Back to Game", ImVec2(-1, 0))) {
|
|
|
|
|
|
showEscapeMenu = false;
|
|
|
|
|
|
showEscapeSettingsNotice = false;
|
2026-02-05 16:01:38 -08:00
|
|
|
|
}
|
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();
|
2026-02-05 16:01:38 -08:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Pet Stable Window
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderStableWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isStableWindowOpen()) 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 / 2.0f - 240.0f, screenH / 2.0f - 180.0f),
|
|
|
|
|
|
ImGuiCond_Once);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(480.0f, 360.0f), ImGuiCond_Once);
|
|
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
|
if (!ImGui::Begin("Pet Stable", &open,
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) {
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
// User closed the window; clear stable state
|
|
|
|
|
|
gameHandler.closeStableWindow();
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const auto& pets = gameHandler.getStabledPets();
|
|
|
|
|
|
uint8_t numSlots = gameHandler.getStableSlots();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TextDisabled("Stable slots: %u", static_cast<unsigned>(numSlots));
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
// Active pets section
|
|
|
|
|
|
bool hasActivePets = false;
|
|
|
|
|
|
for (const auto& p : pets) {
|
|
|
|
|
|
if (p.isActive) { hasActivePets = true; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
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]");
|
|
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Stabled pets section
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.9f, 0.8f, 0.4f, 1.0f), "Stabled Pets");
|
|
|
|
|
|
|
|
|
|
|
|
bool hasStabledPets = false;
|
|
|
|
|
|
for (const auto& p : pets) {
|
|
|
|
|
|
if (!p.isActive) { hasStabledPets = true; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::Button("Refresh")) {
|
|
|
|
|
|
gameHandler.requestStabledPetList();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Close")) {
|
|
|
|
|
|
gameHandler.closeStableWindow();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
gameHandler.closeStableWindow();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 16:59:20 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Taxi Window
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderTaxiWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isTaxiWindowOpen()) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 150), ImGuiCond_Appearing);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
|
|
// 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(ImVec4(0.5f, 1.0f, 0.5f, 1.0f), "Current: %s", curIt->second.name.c_str());
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Text("Select a destination:");
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
2026-02-07 20:02:14 -08:00
|
|
|
|
static uint32_t selectedNodeId = 0;
|
2026-02-07 16:59:20 -08:00
|
|
|
|
int destCount = 0;
|
2026-02-07 20:02:14 -08:00
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
|
|
for (const auto& [nodeId, node] : nodes) {
|
|
|
|
|
|
if (nodeId == currentNode) continue;
|
|
|
|
|
|
if (node.mapId != currentMapId) continue;
|
|
|
|
|
|
if (!taxiData.isNodeKnown(nodeId)) continue;
|
|
|
|
|
|
|
|
|
|
|
|
uint32_t costCopper = gameHandler.getTaxiCostTo(nodeId);
|
|
|
|
|
|
uint32_t gold = costCopper / 10000;
|
|
|
|
|
|
uint32_t silver = (costCopper / 100) % 100;
|
|
|
|
|
|
uint32_t copper = costCopper % 100;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(nodeId));
|
|
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
|
|
|
|
|
bool isSelected = (selectedNodeId == nodeId);
|
2026-02-08 03:05:38 -08:00
|
|
|
|
if (ImGui::Selectable(node.name.c_str(), isSelected,
|
|
|
|
|
|
ImGuiSelectableFlags_SpanAllColumns |
|
|
|
|
|
|
ImGuiSelectableFlags_AllowDoubleClick)) {
|
2026-02-07 20:02:14 -08:00
|
|
|
|
selectedNodeId = nodeId;
|
2026-02-08 03:05:38 -08:00
|
|
|
|
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-02-07 20:02:14 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
2026-03-12 08:15:46 -07:00
|
|
|
|
renderCoinsText(gold, silver, copper);
|
2026-02-07 20:02:14 -08:00
|
|
|
|
|
|
|
|
|
|
ImGui::TableSetColumnIndex(2);
|
|
|
|
|
|
if (ImGui::SmallButton("Fly")) {
|
|
|
|
|
|
selectedNodeId = nodeId;
|
|
|
|
|
|
LOG_INFO("Taxi UI: Fly clicked dest=", nodeId);
|
|
|
|
|
|
gameHandler.activateTaxi(nodeId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
destCount++;
|
2026-02-07 16:59:20 -08:00
|
|
|
|
}
|
2026-02-07 20:02:14 -08:00
|
|
|
|
ImGui::EndTable();
|
2026-02-07 16:59:20 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (destCount == 0) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "No destinations available.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
2026-02-08 03:05:38 -08:00
|
|
|
|
if (selectedNodeId != 0 && ImGui::Button("Fly Selected", ImVec2(-1, 0))) {
|
|
|
|
|
|
LOG_INFO("Taxi UI: Fly Selected dest=", selectedNodeId);
|
|
|
|
|
|
gameHandler.activateTaxi(selectedNodeId);
|
|
|
|
|
|
}
|
2026-02-07 16:59:20 -08:00
|
|
|
|
if (ImGui::Button("Close", ImVec2(-1, 0))) {
|
|
|
|
|
|
gameHandler.closeTaxi();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
gameHandler.closeTaxi();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-13 10:13:54 -07:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Logout Countdown
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderLogoutCountdown(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isLoggingOut()) 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;
|
|
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##LogoutCountdown", nullptr,
|
|
|
|
|
|
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoBringToFrontOnFocus)) {
|
|
|
|
|
|
|
|
|
|
|
|
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)));
|
|
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 17:27:20 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Death Screen
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) {
|
2026-03-12 06:14:18 -07:00
|
|
|
|
if (!gameHandler.showDeathDialog()) {
|
|
|
|
|
|
deathTimerRunning_ = false;
|
|
|
|
|
|
deathElapsed_ = 0.0f;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
float dt = ImGui::GetIO().DeltaTime;
|
|
|
|
|
|
if (!deathTimerRunning_) {
|
|
|
|
|
|
deathElapsed_ = 0.0f;
|
|
|
|
|
|
deathTimerRunning_ = true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
deathElapsed_ += dt;
|
|
|
|
|
|
}
|
2026-02-06 17:27:20 -08: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;
|
|
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
|
|
|
|
|
|
|
// "Release Spirit" dialog centered on screen
|
2026-03-18 00:06:39 -07:00
|
|
|
|
const bool hasSelfRes = gameHandler.canSelfRes();
|
2026-02-06 17:27:20 -08:00
|
|
|
|
float dlgW = 280.0f;
|
2026-03-18 00:06:39 -07:00
|
|
|
|
// Extra height when self-res button is available
|
|
|
|
|
|
float dlgH = hasSelfRes ? 170.0f : 130.0f;
|
2026-02-06 17:27:20 -08:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - dlgW / 2, screenH * 0.35f), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(dlgW, dlgH), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##DeathDialog", nullptr,
|
|
|
|
|
|
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar)) {
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
// Center "You are dead." text
|
|
|
|
|
|
const char* deathText = "You are dead.";
|
|
|
|
|
|
float textW = ImGui::CalcTextSize(deathText).x;
|
|
|
|
|
|
ImGui::SetCursorPosX((dlgW - textW) / 2);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.2f, 0.2f, 1.0f), "%s", deathText);
|
|
|
|
|
|
|
2026-03-12 06:14:18 -07:00
|
|
|
|
// Respawn timer: show how long until forced release
|
|
|
|
|
|
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), "Release in %d:%02d", mins, secs);
|
|
|
|
|
|
float tw = ImGui::CalcTextSize(timerBuf).x;
|
|
|
|
|
|
ImGui::SetCursorPosX((dlgW - tw) / 2);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.65f, 0.65f, 0.65f, 1.0f), "%s", timerBuf);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 17:27:20 -08:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
2026-03-18 00:06:39 -07: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();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 17:27:20 -08: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();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
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();
|
2026-02-07 23:12:24 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 22:31:56 -07:00
|
|
|
|
void GameScreen::renderReclaimCorpseButton(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isPlayerGhost() || !gameHandler.canReclaimCorpse()) 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-03-17 23:52:45 -07:00
|
|
|
|
float delaySec = gameHandler.getCorpseReclaimDelaySec();
|
|
|
|
|
|
bool onDelay = (delaySec > 0.0f);
|
|
|
|
|
|
|
2026-03-09 22:31:56 -07:00
|
|
|
|
float btnW = 220.0f, btnH = 36.0f;
|
2026-03-17 23:52:45 -07:00
|
|
|
|
float winH = btnH + 16.0f + (onDelay ? 20.0f : 0.0f);
|
2026-03-09 22:31:56 -07:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - btnW / 2, screenH * 0.72f), ImGuiCond_Always);
|
2026-03-17 23:52:45 -07:00
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(btnW + 16.0f, winH), ImGuiCond_Always);
|
2026-03-09 22:31:56 -07:00
|
|
|
|
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)) {
|
2026-03-17 23:52:45 -07:00
|
|
|
|
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 22:41:26 -07:00
|
|
|
|
}
|
2026-03-09 22:31:56 -07:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
ImGui::PopStyleVar(2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 23:12:24 -08:00
|
|
|
|
void GameScreen::renderResurrectDialog(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.showResurrectDialog()) 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;
|
|
|
|
|
|
|
|
|
|
|
|
float dlgW = 300.0f;
|
|
|
|
|
|
float dlgH = 110.0f;
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - dlgW / 2, screenH * 0.3f), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(dlgW, dlgH), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.15f, 0.95f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.4f, 0.4f, 0.8f, 1.0f));
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##ResurrectDialog", nullptr,
|
|
|
|
|
|
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar)) {
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
2026-03-09 22:27:24 -07:00
|
|
|
|
const std::string& casterName = gameHandler.getResurrectCasterName();
|
|
|
|
|
|
std::string text = casterName.empty()
|
|
|
|
|
|
? "Return to life?"
|
|
|
|
|
|
: casterName + " wishes to resurrect you.";
|
|
|
|
|
|
float textW = ImGui::CalcTextSize(text.c_str()).x;
|
|
|
|
|
|
ImGui::SetCursorPosX(std::max(4.0f, (dlgW - textW) / 2));
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "%s", text.c_str());
|
2026-02-07 23:12:24 -08:00
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
float btnW = 100.0f;
|
|
|
|
|
|
float spacing = 20.0f;
|
|
|
|
|
|
ImGui::SetCursorPosX((dlgW - btnW * 2 - spacing) / 2);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.5f, 0.2f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.7f, 0.3f, 1.0f));
|
|
|
|
|
|
if (ImGui::Button("Accept", ImVec2(btnW, 30))) {
|
|
|
|
|
|
gameHandler.acceptResurrect();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine(0, spacing);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.2f, 0.2f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.3f, 0.3f, 1.0f));
|
|
|
|
|
|
if (ImGui::Button("Decline", ImVec2(btnW, 30))) {
|
|
|
|
|
|
gameHandler.declineResurrect();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
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();
|
2026-02-06 17:27:20 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 12:53:05 -07:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Talent Wipe Confirm Dialog
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderTalentWipeConfirmDialog(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.showTalentWipeConfirmDialog()) 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;
|
|
|
|
|
|
|
|
|
|
|
|
float dlgW = 340.0f;
|
|
|
|
|
|
float dlgH = 130.0f;
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - dlgW / 2, screenH * 0.3f), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(dlgW, dlgH), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.15f, 0.95f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.8f, 0.7f, 0.2f, 1.0f));
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##TalentWipeDialog", nullptr,
|
|
|
|
|
|
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar)) {
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
uint32_t cost = gameHandler.getTalentWipeCost();
|
|
|
|
|
|
uint32_t gold = cost / 10000;
|
|
|
|
|
|
uint32_t silver = (cost % 10000) / 100;
|
|
|
|
|
|
uint32_t copper = cost % 100;
|
|
|
|
|
|
char costStr[64];
|
|
|
|
|
|
if (gold > 0)
|
|
|
|
|
|
std::snprintf(costStr, sizeof(costStr), "%ug %us %uc", gold, silver, copper);
|
|
|
|
|
|
else if (silver > 0)
|
|
|
|
|
|
std::snprintf(costStr, sizeof(costStr), "%us %uc", silver, copper);
|
|
|
|
|
|
else
|
|
|
|
|
|
std::snprintf(costStr, sizeof(costStr), "%uc", copper);
|
|
|
|
|
|
|
|
|
|
|
|
std::string text = "Reset your talents for ";
|
|
|
|
|
|
text += costStr;
|
|
|
|
|
|
text += "?";
|
|
|
|
|
|
float textW = ImGui::CalcTextSize(text.c_str()).x;
|
|
|
|
|
|
ImGui::SetCursorPosX(std::max(4.0f, (dlgW - textW) / 2));
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.4f, 1.0f), "%s", text.c_str());
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::SetCursorPosX(8.0f);
|
|
|
|
|
|
ImGui::TextDisabled("All talent points will be refunded.");
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
float btnW = 110.0f;
|
|
|
|
|
|
float spacing = 20.0f;
|
|
|
|
|
|
ImGui::SetCursorPosX((dlgW - btnW * 2 - spacing) / 2);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.5f, 0.2f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.7f, 0.3f, 1.0f));
|
|
|
|
|
|
if (ImGui::Button("Confirm", ImVec2(btnW, 30))) {
|
|
|
|
|
|
gameHandler.confirmTalentWipe();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine(0, spacing);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.2f, 0.2f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.3f, 0.3f, 1.0f));
|
|
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(btnW, 30))) {
|
|
|
|
|
|
gameHandler.cancelTalentWipe();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 21:13:27 -07:00
|
|
|
|
void GameScreen::renderPetUnlearnConfirmDialog(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.showPetUnlearnDialog()) 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;
|
|
|
|
|
|
|
|
|
|
|
|
float dlgW = 340.0f;
|
|
|
|
|
|
float dlgH = 130.0f;
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - dlgW / 2, screenH * 0.3f), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(dlgW, dlgH), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.15f, 0.95f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.8f, 0.7f, 0.2f, 1.0f));
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##PetUnlearnDialog", nullptr,
|
|
|
|
|
|
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar)) {
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
uint32_t cost = gameHandler.getPetUnlearnCost();
|
|
|
|
|
|
uint32_t gold = cost / 10000;
|
|
|
|
|
|
uint32_t silver = (cost % 10000) / 100;
|
|
|
|
|
|
uint32_t copper = cost % 100;
|
|
|
|
|
|
char costStr[64];
|
|
|
|
|
|
if (gold > 0)
|
|
|
|
|
|
std::snprintf(costStr, sizeof(costStr), "%ug %us %uc", gold, silver, copper);
|
|
|
|
|
|
else if (silver > 0)
|
|
|
|
|
|
std::snprintf(costStr, sizeof(costStr), "%us %uc", silver, copper);
|
|
|
|
|
|
else
|
|
|
|
|
|
std::snprintf(costStr, sizeof(costStr), "%uc", copper);
|
|
|
|
|
|
|
|
|
|
|
|
std::string text = std::string("Reset your pet's talents for ") + costStr + "?";
|
|
|
|
|
|
float textW = ImGui::CalcTextSize(text.c_str()).x;
|
|
|
|
|
|
ImGui::SetCursorPosX(std::max(4.0f, (dlgW - textW) / 2));
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.4f, 1.0f), "%s", text.c_str());
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::SetCursorPosX(8.0f);
|
|
|
|
|
|
ImGui::TextDisabled("All pet talent points will be refunded.");
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
float btnW = 110.0f;
|
|
|
|
|
|
float spacing = 20.0f;
|
|
|
|
|
|
ImGui::SetCursorPosX((dlgW - btnW * 2 - spacing) / 2);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.5f, 0.2f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.7f, 0.3f, 1.0f));
|
|
|
|
|
|
if (ImGui::Button("Confirm##petunlearn", ImVec2(btnW, 30))) {
|
|
|
|
|
|
gameHandler.confirmPetUnlearn();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine(0, spacing);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.2f, 0.2f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.3f, 0.3f, 1.0f));
|
|
|
|
|
|
if (ImGui::Button("Cancel##petunlearn", ImVec2(btnW, 30))) {
|
|
|
|
|
|
gameHandler.cancelPetUnlearn();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 16:11:00 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Settings Window
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderSettingsWindow() {
|
|
|
|
|
|
if (!showSettingsWindow) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
2026-02-05 16:14:11 -08:00
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
2026-02-05 16:11:00 -08:00
|
|
|
|
if (!window) return;
|
|
|
|
|
|
|
|
|
|
|
|
static const int kResolutions[][2] = {
|
|
|
|
|
|
{1280, 720},
|
|
|
|
|
|
{1600, 900},
|
|
|
|
|
|
{1920, 1080},
|
|
|
|
|
|
{2560, 1440},
|
|
|
|
|
|
{3840, 2160},
|
|
|
|
|
|
};
|
|
|
|
|
|
static const int kResCount = sizeof(kResolutions) / sizeof(kResolutions[0]);
|
2026-02-05 17:40:15 -08:00
|
|
|
|
constexpr int kDefaultResW = 1920;
|
|
|
|
|
|
constexpr int kDefaultResH = 1080;
|
|
|
|
|
|
constexpr bool kDefaultFullscreen = false;
|
|
|
|
|
|
constexpr bool kDefaultVsync = true;
|
2026-02-23 08:40:16 -08:00
|
|
|
|
constexpr bool kDefaultShadows = true;
|
2026-02-05 17:40:15 -08:00
|
|
|
|
constexpr int kDefaultMusicVolume = 30;
|
|
|
|
|
|
constexpr float kDefaultMouseSensitivity = 0.2f;
|
|
|
|
|
|
constexpr bool kDefaultInvertMouse = false;
|
2026-02-21 01:26:16 -08:00
|
|
|
|
constexpr int kDefaultGroundClutterDensity = 100;
|
2026-02-05 17:40:15 -08:00
|
|
|
|
|
|
|
|
|
|
int defaultResIndex = 0;
|
|
|
|
|
|
for (int i = 0; i < kResCount; i++) {
|
|
|
|
|
|
if (kResolutions[i][0] == kDefaultResW && kResolutions[i][1] == kDefaultResH) {
|
|
|
|
|
|
defaultResIndex = i;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-05 16:11:00 -08:00
|
|
|
|
|
|
|
|
|
|
if (!settingsInit) {
|
|
|
|
|
|
pendingFullscreen = window->isFullscreen();
|
|
|
|
|
|
pendingVsync = window->isVsyncEnabled();
|
2026-02-05 17:32:21 -08:00
|
|
|
|
if (renderer) {
|
2026-02-23 08:40:16 -08:00
|
|
|
|
renderer->setShadowsEnabled(pendingShadows);
|
2026-03-06 20:38:58 -08:00
|
|
|
|
renderer->setShadowDistance(pendingShadowDistance);
|
2026-02-17 17:37:20 -08:00
|
|
|
|
// Read non-volume settings from actual state (volumes come from saved settings)
|
2026-02-05 17:40:15 -08:00
|
|
|
|
if (auto* cameraController = renderer->getCameraController()) {
|
|
|
|
|
|
pendingMouseSensitivity = cameraController->getMouseSensitivity();
|
|
|
|
|
|
pendingInvertMouse = cameraController->isInvertMouse();
|
2026-02-23 08:09:27 -08:00
|
|
|
|
cameraController->setExtendedZoom(pendingExtendedZoom);
|
2026-02-05 17:40:15 -08:00
|
|
|
|
}
|
2026-02-05 17:32:21 -08:00
|
|
|
|
}
|
2026-02-05 16:11:00 -08:00
|
|
|
|
pendingResIndex = 0;
|
|
|
|
|
|
int curW = window->getWidth();
|
|
|
|
|
|
int curH = window->getHeight();
|
|
|
|
|
|
for (int i = 0; i < kResCount; i++) {
|
|
|
|
|
|
if (kResolutions[i][0] == curW && kResolutions[i][1] == curH) {
|
|
|
|
|
|
pendingResIndex = i;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-18 20:02:12 -08:00
|
|
|
|
pendingUiOpacity = static_cast<int>(std::lround(uiOpacity_ * 100.0f));
|
2026-02-07 20:51:53 -08:00
|
|
|
|
pendingMinimapRotate = minimapRotate_;
|
2026-02-09 17:39:21 -08:00
|
|
|
|
pendingMinimapSquare = minimapSquare_;
|
2026-02-20 16:40:22 -08:00
|
|
|
|
pendingMinimapNpcDots = minimapNpcDots_;
|
2026-03-11 19:45:03 -07:00
|
|
|
|
pendingShowLatencyMeter = showLatencyMeter_;
|
2026-02-07 20:51:53 -08:00
|
|
|
|
if (renderer) {
|
|
|
|
|
|
if (auto* minimap = renderer->getMinimap()) {
|
|
|
|
|
|
minimap->setRotateWithCamera(minimapRotate_);
|
2026-02-09 17:39:21 -08:00
|
|
|
|
minimap->setSquareShape(minimapSquare_);
|
2026-02-07 20:51:53 -08:00
|
|
|
|
}
|
2026-02-17 16:26:49 -08:00
|
|
|
|
if (auto* zm = renderer->getZoneManager()) {
|
|
|
|
|
|
pendingUseOriginalSoundtrack = zm->getUseOriginalSoundtrack();
|
|
|
|
|
|
}
|
2026-02-07 20:51:53 -08:00
|
|
|
|
}
|
2026-02-05 16:11:00 -08:00
|
|
|
|
settingsInit = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
|
|
float screenW = io.DisplaySize.x;
|
|
|
|
|
|
float screenH = io.DisplaySize.y;
|
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
|
|
|
|
ImVec2 size(520.0f, std::min(screenH * 0.9f, 720.0f));
|
2026-02-05 16:11:00 -08:00
|
|
|
|
ImVec2 pos((screenW - size.x) * 0.5f, (screenH - size.y) * 0.5f);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(pos, ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(size, ImGuiCond_Always);
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar;
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##SettingsWindow", nullptr, flags)) {
|
|
|
|
|
|
ImGui::Text("Settings");
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
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
|
|
|
|
if (ImGui::BeginTabBar("SettingsTabs", ImGuiTabBarFlags_None)) {
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// VIDEO TAB
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
if (ImGui::BeginTabItem("Video")) {
|
|
|
|
|
|
ImGui::Spacing();
|
2026-02-09 17:12:35 -08:00
|
|
|
|
|
2026-03-11 15:21:48 -07:00
|
|
|
|
// Graphics Quality Presets
|
|
|
|
|
|
{
|
|
|
|
|
|
const char* presetLabels[] = { "Custom", "Low", "Medium", "High", "Ultra" };
|
|
|
|
|
|
int presetIdx = static_cast<int>(pendingGraphicsPreset);
|
|
|
|
|
|
if (ImGui::Combo("Quality Preset", &presetIdx, presetLabels, 5)) {
|
|
|
|
|
|
pendingGraphicsPreset = static_cast<GraphicsPreset>(presetIdx);
|
|
|
|
|
|
if (pendingGraphicsPreset != GraphicsPreset::CUSTOM) {
|
|
|
|
|
|
applyGraphicsPreset(pendingGraphicsPreset);
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::TextDisabled("Adjust these for custom settings");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
2026-02-09 17:12:35 -08:00
|
|
|
|
if (ImGui::Checkbox("Fullscreen", &pendingFullscreen)) {
|
|
|
|
|
|
window->setFullscreen(pendingFullscreen);
|
2026-03-11 15:21:48 -07:00
|
|
|
|
updateGraphicsPresetFromCurrentSettings();
|
2026-02-09 17:12:35 -08:00
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::Checkbox("VSync", &pendingVsync)) {
|
|
|
|
|
|
window->setVsync(pendingVsync);
|
2026-03-11 15:21:48 -07:00
|
|
|
|
updateGraphicsPresetFromCurrentSettings();
|
2026-02-09 17:12:35 -08:00
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::Checkbox("Shadows", &pendingShadows)) {
|
|
|
|
|
|
if (renderer) renderer->setShadowsEnabled(pendingShadows);
|
2026-03-11 15:21:48 -07:00
|
|
|
|
updateGraphicsPresetFromCurrentSettings();
|
2026-02-09 17:12:35 -08:00
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
2026-03-06 20:38:58 -08:00
|
|
|
|
if (pendingShadows) {
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::SetNextItemWidth(150.0f);
|
2026-03-07 22:29:06 -08:00
|
|
|
|
if (ImGui::SliderFloat("Distance##shadow", &pendingShadowDistance, 40.0f, 500.0f, "%.0f")) {
|
2026-03-06 20:38:58 -08:00
|
|
|
|
if (renderer) renderer->setShadowDistance(pendingShadowDistance);
|
2026-03-11 15:21:48 -07:00
|
|
|
|
updateGraphicsPresetFromCurrentSettings();
|
2026-03-06 20:38:58 -08:00
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-09 20:58:49 -07:00
|
|
|
|
{
|
|
|
|
|
|
bool fsrActive = renderer && (renderer->isFSREnabled() || renderer->isFSR2Enabled());
|
|
|
|
|
|
if (!fsrActive && pendingWaterRefraction) {
|
|
|
|
|
|
// FSR was disabled while refraction was on — auto-disable
|
|
|
|
|
|
pendingWaterRefraction = false;
|
|
|
|
|
|
if (renderer) renderer->setWaterRefractionEnabled(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!fsrActive) ImGui::BeginDisabled();
|
|
|
|
|
|
if (ImGui::Checkbox("Water Refraction (requires FSR)", &pendingWaterRefraction)) {
|
|
|
|
|
|
if (renderer) renderer->setWaterRefractionEnabled(pendingWaterRefraction);
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!fsrActive) ImGui::EndDisabled();
|
2026-03-06 19:15:34 -08:00
|
|
|
|
}
|
2026-02-22 02:59:24 -08:00
|
|
|
|
{
|
|
|
|
|
|
const char* aaLabels[] = { "Off", "2x MSAA", "4x MSAA", "8x MSAA" };
|
2026-03-08 01:22:15 -08:00
|
|
|
|
bool fsr2Active = renderer && renderer->isFSR2Enabled();
|
|
|
|
|
|
if (fsr2Active) {
|
|
|
|
|
|
ImGui::BeginDisabled();
|
|
|
|
|
|
int disabled = 0;
|
Add FSR3 Generic API path and harden runtime diagnostics
- AmdFsr3Runtime now probes both the legacy ffxFsr3* API and the newer
generic ffxCreateContext/ffxDispatch API; selects whichever the loaded
runtime library exports (GenericApi takes priority fallback)
- Generic API path implements full upscale + frame-generation context
creation, configure, dispatch, and destroy lifecycle
- dlopen error captured and surfaced in lastError_ on Linux so runtime
initialization failures are actionable
- FSR3 runtime init failure log now includes path kind, error string,
and loaded library path for easier debugging
- tools/generate_ffx_sdk_vk_permutations.sh added: auto-bootstraps
missing VK permutation headers; DXC auto-downloaded on Linux/Windows
MSYS2; macOS reads from PATH (CI installs via brew dxc)
- CMakeLists: add upscalers/include to probe include dirs, invoke
permutation script before SDK build, scope FFX pragma/ODR warning
suppressions to affected TUs, add runtime-copy dependency on wowee
- UI labels updated from "FSR2" → "FSR3" in settings, tuning panel,
performance HUD, and combo boxes
- CI macOS job now installs dxc via Homebrew for permutation codegen
2026-03-09 12:51:59 -07:00
|
|
|
|
ImGui::Combo("Anti-Aliasing (FSR3)", &disabled, "Off (FSR3 active)\0", 1);
|
2026-03-08 01:22:15 -08:00
|
|
|
|
ImGui::EndDisabled();
|
|
|
|
|
|
} else if (ImGui::Combo("Anti-Aliasing", &pendingAntiAliasing, aaLabels, 4)) {
|
2026-02-22 02:59:24 -08:00
|
|
|
|
static const VkSampleCountFlagBits aaSamples[] = {
|
|
|
|
|
|
VK_SAMPLE_COUNT_1_BIT, VK_SAMPLE_COUNT_2_BIT,
|
|
|
|
|
|
VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_8_BIT
|
|
|
|
|
|
};
|
|
|
|
|
|
if (renderer) renderer->setMsaaSamples(aaSamples[pendingAntiAliasing]);
|
2026-03-11 15:21:48 -07:00
|
|
|
|
updateGraphicsPresetFromCurrentSettings();
|
2026-02-22 02:59:24 -08:00
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
2026-03-12 19:27:00 -07:00
|
|
|
|
// FXAA — post-process, combinable with MSAA or FSR3
|
|
|
|
|
|
{
|
2026-03-12 16:43:48 -07:00
|
|
|
|
if (ImGui::Checkbox("FXAA (post-process)", &pendingFXAA)) {
|
|
|
|
|
|
if (renderer) renderer->setFXAAEnabled(pendingFXAA);
|
|
|
|
|
|
updateGraphicsPresetFromCurrentSettings();
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
2026-03-12 19:27:00 -07:00
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
if (fsr2Active)
|
|
|
|
|
|
ImGui::SetTooltip("FXAA applies spatial anti-aliasing after FSR3 upscaling.\nFSR3 + FXAA is the recommended ultra-quality combination.");
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::SetTooltip("FXAA smooths jagged edges as a post-process pass.\nCan be combined with MSAA for extra quality.");
|
|
|
|
|
|
}
|
2026-03-12 16:43:48 -07:00
|
|
|
|
}
|
2026-02-22 02:59:24 -08:00
|
|
|
|
}
|
2026-03-07 23:13:01 -08:00
|
|
|
|
// FSR Upscaling
|
2026-03-07 22:03:28 -08:00
|
|
|
|
{
|
Add FSR3 Generic API path and harden runtime diagnostics
- AmdFsr3Runtime now probes both the legacy ffxFsr3* API and the newer
generic ffxCreateContext/ffxDispatch API; selects whichever the loaded
runtime library exports (GenericApi takes priority fallback)
- Generic API path implements full upscale + frame-generation context
creation, configure, dispatch, and destroy lifecycle
- dlopen error captured and surfaced in lastError_ on Linux so runtime
initialization failures are actionable
- FSR3 runtime init failure log now includes path kind, error string,
and loaded library path for easier debugging
- tools/generate_ffx_sdk_vk_permutations.sh added: auto-bootstraps
missing VK permutation headers; DXC auto-downloaded on Linux/Windows
MSYS2; macOS reads from PATH (CI installs via brew dxc)
- CMakeLists: add upscalers/include to probe include dirs, invoke
permutation script before SDK build, scope FFX pragma/ODR warning
suppressions to affected TUs, add runtime-copy dependency on wowee
- UI labels updated from "FSR2" → "FSR3" in settings, tuning panel,
performance HUD, and combo boxes
- CI macOS job now installs dxc via Homebrew for permutation codegen
2026-03-09 12:51:59 -07:00
|
|
|
|
// FSR mode selection: Off, FSR 1.0 (Spatial), FSR 3.x (Temporal)
|
|
|
|
|
|
const char* fsrModeLabels[] = { "Off", "FSR 1.0 (Spatial)", "FSR 3.x (Temporal)" };
|
2026-03-08 20:22:11 -07:00
|
|
|
|
int fsrMode = pendingUpscalingMode;
|
2026-03-07 23:13:01 -08:00
|
|
|
|
if (ImGui::Combo("Upscaling", &fsrMode, fsrModeLabels, 3)) {
|
2026-03-08 20:22:11 -07:00
|
|
|
|
pendingUpscalingMode = fsrMode;
|
2026-03-07 23:13:01 -08:00
|
|
|
|
pendingFSR = (fsrMode == 1);
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
renderer->setFSREnabled(fsrMode == 1);
|
|
|
|
|
|
renderer->setFSR2Enabled(fsrMode == 2);
|
|
|
|
|
|
}
|
2026-03-07 22:03:28 -08:00
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
2026-03-07 23:13:01 -08:00
|
|
|
|
if (fsrMode > 0) {
|
2026-03-08 19:33:07 -07:00
|
|
|
|
if (fsrMode == 2 && renderer) {
|
Add FSR3 Generic API path and harden runtime diagnostics
- AmdFsr3Runtime now probes both the legacy ffxFsr3* API and the newer
generic ffxCreateContext/ffxDispatch API; selects whichever the loaded
runtime library exports (GenericApi takes priority fallback)
- Generic API path implements full upscale + frame-generation context
creation, configure, dispatch, and destroy lifecycle
- dlopen error captured and surfaced in lastError_ on Linux so runtime
initialization failures are actionable
- FSR3 runtime init failure log now includes path kind, error string,
and loaded library path for easier debugging
- tools/generate_ffx_sdk_vk_permutations.sh added: auto-bootstraps
missing VK permutation headers; DXC auto-downloaded on Linux/Windows
MSYS2; macOS reads from PATH (CI installs via brew dxc)
- CMakeLists: add upscalers/include to probe include dirs, invoke
permutation script before SDK build, scope FFX pragma/ODR warning
suppressions to affected TUs, add runtime-copy dependency on wowee
- UI labels updated from "FSR2" → "FSR3" in settings, tuning panel,
performance HUD, and combo boxes
- CI macOS job now installs dxc via Homebrew for permutation codegen
2026-03-09 12:51:59 -07:00
|
|
|
|
ImGui::TextDisabled("FSR3 backend: %s",
|
2026-03-08 19:33:07 -07:00
|
|
|
|
renderer->isAmdFsr2SdkAvailable() ? "AMD FidelityFX SDK" : "Internal fallback");
|
2026-03-08 22:53:21 -07:00
|
|
|
|
if (renderer->isAmdFsr3FramegenSdkAvailable()) {
|
|
|
|
|
|
if (ImGui::Checkbox("AMD FSR3 Frame Generation (Experimental)", &pendingAMDFramegen)) {
|
|
|
|
|
|
renderer->setAmdFsr3FramegenEnabled(pendingAMDFramegen);
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
2026-03-08 23:03:45 -07:00
|
|
|
|
const char* runtimeStatus = "Unavailable";
|
|
|
|
|
|
if (renderer->isAmdFsr3FramegenRuntimeActive()) {
|
|
|
|
|
|
runtimeStatus = "Active";
|
|
|
|
|
|
} else if (renderer->isAmdFsr3FramegenRuntimeReady()) {
|
2026-03-09 00:01:45 -07:00
|
|
|
|
runtimeStatus = "Ready";
|
2026-03-08 23:03:45 -07:00
|
|
|
|
} else {
|
2026-03-09 00:01:45 -07:00
|
|
|
|
runtimeStatus = "Unavailable";
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::TextDisabled("Runtime: %s (%s)",
|
|
|
|
|
|
runtimeStatus, renderer->getAmdFsr3FramegenRuntimePath());
|
|
|
|
|
|
if (!renderer->isAmdFsr3FramegenRuntimeReady()) {
|
|
|
|
|
|
const std::string& runtimeErr = renderer->getAmdFsr3FramegenRuntimeError();
|
|
|
|
|
|
if (!runtimeErr.empty()) {
|
|
|
|
|
|
ImGui::TextDisabled("Reason: %s", runtimeErr.c_str());
|
|
|
|
|
|
}
|
2026-03-08 23:03:45 -07:00
|
|
|
|
}
|
2026-03-08 22:53:21 -07:00
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::BeginDisabled();
|
|
|
|
|
|
bool disabledFg = false;
|
|
|
|
|
|
ImGui::Checkbox("AMD FSR3 Frame Generation (Experimental)", &disabledFg);
|
|
|
|
|
|
ImGui::EndDisabled();
|
|
|
|
|
|
ImGui::TextDisabled("Requires FidelityFX-SDK framegen headers.");
|
|
|
|
|
|
}
|
2026-03-08 19:33:07 -07:00
|
|
|
|
}
|
2026-03-08 21:34:31 -07:00
|
|
|
|
const char* fsrQualityLabels[] = { "Native (100%)", "Ultra Quality (77%)", "Quality (67%)", "Balanced (59%)" };
|
2026-03-08 21:17:04 -07:00
|
|
|
|
static const float fsrScaleFactors[] = { 0.77f, 0.67f, 0.59f, 1.00f };
|
2026-03-08 21:34:31 -07:00
|
|
|
|
static const int displayToInternal[] = { 3, 0, 1, 2 };
|
2026-03-08 21:17:04 -07:00
|
|
|
|
pendingFSRQuality = std::clamp(pendingFSRQuality, 0, 3);
|
2026-03-08 21:34:31 -07:00
|
|
|
|
int fsrQualityDisplay = 0;
|
|
|
|
|
|
for (int i = 0; i < 4; ++i) {
|
|
|
|
|
|
if (displayToInternal[i] == pendingFSRQuality) {
|
|
|
|
|
|
fsrQualityDisplay = i;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::Combo("FSR Quality", &fsrQualityDisplay, fsrQualityLabels, 4)) {
|
|
|
|
|
|
pendingFSRQuality = displayToInternal[fsrQualityDisplay];
|
2026-03-07 22:03:28 -08:00
|
|
|
|
if (renderer) renderer->setFSRQuality(fsrScaleFactors[pendingFSRQuality]);
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::SliderFloat("FSR Sharpness", &pendingFSRSharpness, 0.0f, 2.0f, "%.1f")) {
|
|
|
|
|
|
if (renderer) renderer->setFSRSharpness(pendingFSRSharpness);
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
2026-03-08 20:56:22 -07:00
|
|
|
|
if (fsrMode == 2) {
|
Add FSR3 Generic API path and harden runtime diagnostics
- AmdFsr3Runtime now probes both the legacy ffxFsr3* API and the newer
generic ffxCreateContext/ffxDispatch API; selects whichever the loaded
runtime library exports (GenericApi takes priority fallback)
- Generic API path implements full upscale + frame-generation context
creation, configure, dispatch, and destroy lifecycle
- dlopen error captured and surfaced in lastError_ on Linux so runtime
initialization failures are actionable
- FSR3 runtime init failure log now includes path kind, error string,
and loaded library path for easier debugging
- tools/generate_ffx_sdk_vk_permutations.sh added: auto-bootstraps
missing VK permutation headers; DXC auto-downloaded on Linux/Windows
MSYS2; macOS reads from PATH (CI installs via brew dxc)
- CMakeLists: add upscalers/include to probe include dirs, invoke
permutation script before SDK build, scope FFX pragma/ODR warning
suppressions to affected TUs, add runtime-copy dependency on wowee
- UI labels updated from "FSR2" → "FSR3" in settings, tuning panel,
performance HUD, and combo boxes
- CI macOS job now installs dxc via Homebrew for permutation codegen
2026-03-09 12:51:59 -07:00
|
|
|
|
ImGui::SeparatorText("FSR3 Tuning");
|
2026-03-08 20:56:22 -07:00
|
|
|
|
if (ImGui::SliderFloat("Jitter Sign", &pendingFSR2JitterSign, -2.0f, 2.0f, "%.2f")) {
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
renderer->setFSR2DebugTuning(
|
|
|
|
|
|
pendingFSR2JitterSign,
|
|
|
|
|
|
pendingFSR2MotionVecScaleX,
|
|
|
|
|
|
pendingFSR2MotionVecScaleY);
|
|
|
|
|
|
}
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
2026-03-08 21:34:31 -07:00
|
|
|
|
ImGui::TextDisabled("Tip: 0.38 is the current recommended default.");
|
2026-03-08 20:56:22 -07:00
|
|
|
|
}
|
2026-03-07 22:03:28 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-21 01:26:16 -08:00
|
|
|
|
if (ImGui::SliderInt("Ground Clutter Density", &pendingGroundClutterDensity, 0, 150, "%d%%")) {
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
if (auto* tm = renderer->getTerrainManager()) {
|
|
|
|
|
|
tm->setGroundClutterDensityScale(static_cast<float>(pendingGroundClutterDensity) / 100.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
2026-02-23 01:10:58 -08:00
|
|
|
|
if (ImGui::Checkbox("Normal Mapping", &pendingNormalMapping)) {
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
if (auto* wr = renderer->getWMORenderer()) {
|
|
|
|
|
|
wr->setNormalMappingEnabled(pendingNormalMapping);
|
|
|
|
|
|
}
|
2026-02-23 01:40:23 -08:00
|
|
|
|
if (auto* cr = renderer->getCharacterRenderer()) {
|
|
|
|
|
|
cr->setNormalMappingEnabled(pendingNormalMapping);
|
|
|
|
|
|
}
|
2026-02-23 01:10:58 -08:00
|
|
|
|
}
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
2026-02-23 01:18:42 -08:00
|
|
|
|
if (pendingNormalMapping) {
|
|
|
|
|
|
if (ImGui::SliderFloat("Normal Map Strength", &pendingNormalMapStrength, 0.0f, 2.0f, "%.1f")) {
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
if (auto* wr = renderer->getWMORenderer()) {
|
|
|
|
|
|
wr->setNormalMapStrength(pendingNormalMapStrength);
|
|
|
|
|
|
}
|
2026-02-23 01:40:23 -08:00
|
|
|
|
if (auto* cr = renderer->getCharacterRenderer()) {
|
|
|
|
|
|
cr->setNormalMapStrength(pendingNormalMapStrength);
|
|
|
|
|
|
}
|
2026-02-23 01:18:42 -08:00
|
|
|
|
}
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-23 01:10:58 -08:00
|
|
|
|
if (ImGui::Checkbox("Parallax Mapping", &pendingPOM)) {
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
if (auto* wr = renderer->getWMORenderer()) {
|
|
|
|
|
|
wr->setPOMEnabled(pendingPOM);
|
|
|
|
|
|
}
|
2026-02-23 01:40:23 -08:00
|
|
|
|
if (auto* cr = renderer->getCharacterRenderer()) {
|
|
|
|
|
|
cr->setPOMEnabled(pendingPOM);
|
|
|
|
|
|
}
|
2026-02-23 01:10:58 -08:00
|
|
|
|
}
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (pendingPOM) {
|
|
|
|
|
|
const char* pomLabels[] = { "Low", "Medium", "High" };
|
|
|
|
|
|
if (ImGui::Combo("Parallax Quality", &pendingPOMQuality, pomLabels, 3)) {
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
if (auto* wr = renderer->getWMORenderer()) {
|
|
|
|
|
|
wr->setPOMQuality(pendingPOMQuality);
|
|
|
|
|
|
}
|
2026-02-23 01:40:23 -08:00
|
|
|
|
if (auto* cr = renderer->getCharacterRenderer()) {
|
|
|
|
|
|
cr->setPOMQuality(pendingPOMQuality);
|
|
|
|
|
|
}
|
2026-02-23 01:10:58 -08:00
|
|
|
|
}
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
|
|
const char* resLabel = "Resolution";
|
|
|
|
|
|
const char* resItems[kResCount];
|
|
|
|
|
|
char resBuf[kResCount][16];
|
|
|
|
|
|
for (int i = 0; i < kResCount; i++) {
|
|
|
|
|
|
snprintf(resBuf[i], sizeof(resBuf[i]), "%dx%d", kResolutions[i][0], kResolutions[i][1]);
|
|
|
|
|
|
resItems[i] = resBuf[i];
|
|
|
|
|
|
}
|
2026-02-09 17:12:35 -08:00
|
|
|
|
if (ImGui::Combo(resLabel, &pendingResIndex, resItems, kResCount)) {
|
|
|
|
|
|
window->applyResolution(kResolutions[pendingResIndex][0], kResolutions[pendingResIndex][1]);
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
2026-02-05 16:11:00 -08:00
|
|
|
|
|
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
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::Spacing();
|
2026-02-05 16:11:00 -08:00
|
|
|
|
|
2026-03-17 09:04:53 -07:00
|
|
|
|
ImGui::SetNextItemWidth(200.0f);
|
|
|
|
|
|
if (ImGui::SliderInt("Brightness", &pendingBrightness, 0, 100, "%d%%")) {
|
|
|
|
|
|
if (renderer) renderer->setBrightness(static_cast<float>(pendingBrightness) / 50.0f);
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
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
|
|
|
|
if (ImGui::Button("Restore Video Defaults", ImVec2(-1, 0))) {
|
|
|
|
|
|
pendingFullscreen = kDefaultFullscreen;
|
|
|
|
|
|
pendingVsync = kDefaultVsync;
|
|
|
|
|
|
pendingShadows = kDefaultShadows;
|
2026-03-07 22:29:06 -08:00
|
|
|
|
pendingShadowDistance = 300.0f;
|
2026-02-21 01:26:16 -08:00
|
|
|
|
pendingGroundClutterDensity = kDefaultGroundClutterDensity;
|
2026-02-22 02:59:24 -08:00
|
|
|
|
pendingAntiAliasing = 0;
|
2026-02-23 01:10:58 -08:00
|
|
|
|
pendingNormalMapping = true;
|
2026-02-23 01:21:58 -08:00
|
|
|
|
pendingNormalMapStrength = 0.8f;
|
2026-02-23 01:23:24 -08:00
|
|
|
|
pendingPOM = true;
|
2026-02-23 01:10:58 -08:00
|
|
|
|
pendingPOMQuality = 1;
|
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
|
|
|
|
pendingResIndex = defaultResIndex;
|
2026-03-17 09:04:53 -07:00
|
|
|
|
pendingBrightness = 50;
|
2026-02-09 17:12:35 -08:00
|
|
|
|
window->setFullscreen(pendingFullscreen);
|
|
|
|
|
|
window->setVsync(pendingVsync);
|
|
|
|
|
|
window->applyResolution(kResolutions[pendingResIndex][0], kResolutions[pendingResIndex][1]);
|
2026-03-17 09:04:53 -07:00
|
|
|
|
if (renderer) renderer->setBrightness(1.0f);
|
2026-03-06 19:15:34 -08:00
|
|
|
|
pendingWaterRefraction = false;
|
2026-03-06 20:38:58 -08:00
|
|
|
|
if (renderer) {
|
|
|
|
|
|
renderer->setShadowsEnabled(pendingShadows);
|
|
|
|
|
|
renderer->setShadowDistance(pendingShadowDistance);
|
|
|
|
|
|
}
|
2026-03-06 19:15:34 -08:00
|
|
|
|
if (renderer) renderer->setWaterRefractionEnabled(pendingWaterRefraction);
|
2026-02-22 02:59:24 -08:00
|
|
|
|
if (renderer) renderer->setMsaaSamples(VK_SAMPLE_COUNT_1_BIT);
|
2026-02-21 01:26:16 -08:00
|
|
|
|
if (renderer) {
|
|
|
|
|
|
if (auto* tm = renderer->getTerrainManager()) {
|
|
|
|
|
|
tm->setGroundClutterDensityScale(static_cast<float>(pendingGroundClutterDensity) / 100.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-23 01:10:58 -08:00
|
|
|
|
if (renderer) {
|
|
|
|
|
|
if (auto* wr = renderer->getWMORenderer()) {
|
|
|
|
|
|
wr->setNormalMappingEnabled(pendingNormalMapping);
|
2026-02-23 01:18:42 -08:00
|
|
|
|
wr->setNormalMapStrength(pendingNormalMapStrength);
|
2026-02-23 01:10:58 -08:00
|
|
|
|
wr->setPOMEnabled(pendingPOM);
|
|
|
|
|
|
wr->setPOMQuality(pendingPOMQuality);
|
|
|
|
|
|
}
|
2026-02-23 01:40:23 -08:00
|
|
|
|
if (auto* cr = renderer->getCharacterRenderer()) {
|
|
|
|
|
|
cr->setNormalMappingEnabled(pendingNormalMapping);
|
|
|
|
|
|
cr->setNormalMapStrength(pendingNormalMapStrength);
|
|
|
|
|
|
cr->setPOMEnabled(pendingPOM);
|
|
|
|
|
|
cr->setPOMQuality(pendingPOMQuality);
|
|
|
|
|
|
}
|
2026-02-23 01:10:58 -08:00
|
|
|
|
}
|
2026-02-09 17:12:35 -08:00
|
|
|
|
saveSettings();
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 15:45:35 -07:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// INTERFACE TAB
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
if (ImGui::BeginTabItem("Interface")) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::BeginChild("InterfaceSettings", ImVec2(0, 360), true);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SeparatorText("Action Bars");
|
|
|
|
|
|
ImGui::Spacing();
|
2026-03-11 22:39:59 -07:00
|
|
|
|
ImGui::SetNextItemWidth(200.0f);
|
|
|
|
|
|
if (ImGui::SliderFloat("Action Bar Scale", &pendingActionBarScale, 0.5f, 1.5f, "%.2fx")) {
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Spacing();
|
2026-03-10 15:45:35 -07:00
|
|
|
|
|
|
|
|
|
|
if (ImGui::Checkbox("Show Second Action Bar", &pendingShowActionBar2)) {
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextDisabled("(Shift+1 through Shift+=)");
|
|
|
|
|
|
|
|
|
|
|
|
if (pendingShowActionBar2) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::TextUnformatted("Second Bar Position Offset");
|
|
|
|
|
|
ImGui::SetNextItemWidth(160.0f);
|
|
|
|
|
|
if (ImGui::SliderFloat("Horizontal##bar2x", &pendingActionBar2OffsetX, -600.0f, 600.0f, "%.0f px")) {
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SetNextItemWidth(160.0f);
|
|
|
|
|
|
if (ImGui::SliderFloat("Vertical##bar2y", &pendingActionBar2OffsetY, -400.0f, 400.0f, "%.0f px")) {
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::Button("Reset Position##bar2")) {
|
|
|
|
|
|
pendingActionBar2OffsetX = 0.0f;
|
|
|
|
|
|
pendingActionBar2OffsetY = 0.0f;
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 15:56:41 -07:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
if (ImGui::Checkbox("Show Right Side Bar", &pendingShowRightBar)) {
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextDisabled("(Slots 25-36)");
|
|
|
|
|
|
if (pendingShowRightBar) {
|
|
|
|
|
|
ImGui::SetNextItemWidth(160.0f);
|
|
|
|
|
|
if (ImGui::SliderFloat("Vertical Offset##rbar", &pendingRightBarOffsetY, -400.0f, 400.0f, "%.0f px")) {
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
if (ImGui::Checkbox("Show Left Side Bar", &pendingShowLeftBar)) {
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextDisabled("(Slots 37-48)");
|
|
|
|
|
|
if (pendingShowLeftBar) {
|
|
|
|
|
|
ImGui::SetNextItemWidth(160.0f);
|
|
|
|
|
|
if (ImGui::SliderFloat("Vertical Offset##lbar", &pendingLeftBarOffsetY, -400.0f, 400.0f, "%.0f px")) {
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 22:49:54 -07:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::SeparatorText("Nameplates");
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::SetNextItemWidth(200.0f);
|
|
|
|
|
|
if (ImGui::SliderFloat("Nameplate Scale", &nameplateScale_, 0.5f, 2.0f, "%.2fx")) {
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 19:45:03 -07:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::SeparatorText("Network");
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
if (ImGui::Checkbox("Show Latency Meter", &pendingShowLatencyMeter)) {
|
|
|
|
|
|
showLatencyMeter_ = pendingShowLatencyMeter;
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextDisabled("(ms indicator near minimap)");
|
|
|
|
|
|
|
2026-03-12 04:04:27 -07:00
|
|
|
|
if (ImGui::Checkbox("Show DPS/HPS Meter", &showDPSMeter_)) {
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextDisabled("(damage/healing per second above action bar)");
|
|
|
|
|
|
|
2026-03-12 15:25:07 -07:00
|
|
|
|
if (ImGui::Checkbox("Show Cooldown Tracker", &showCooldownTracker_)) {
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextDisabled("(active spell cooldowns near action bar)");
|
|
|
|
|
|
|
2026-03-12 03:21:49 -07:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::SeparatorText("Screen Effects");
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
if (ImGui::Checkbox("Damage Flash", &damageFlashEnabled_)) {
|
|
|
|
|
|
if (!damageFlashEnabled_) damageFlashAlpha_ = 0.0f;
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextDisabled("(red vignette on taking damage)");
|
|
|
|
|
|
|
2026-03-12 07:15:08 -07:00
|
|
|
|
if (ImGui::Checkbox("Low Health Vignette", &lowHealthVignetteEnabled_)) {
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextDisabled("(pulsing red edges below 20%% HP)");
|
|
|
|
|
|
|
2026-03-10 15:45:35 -07:00
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
// ============================================================
|
|
|
|
|
|
// AUDIO TAB
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
if (ImGui::BeginTabItem("Audio")) {
|
|
|
|
|
|
ImGui::Spacing();
|
2026-02-09 17:12:35 -08:00
|
|
|
|
ImGui::BeginChild("AudioSettings", ImVec2(0, 360), true);
|
|
|
|
|
|
|
|
|
|
|
|
// Helper lambda to apply audio settings
|
|
|
|
|
|
auto applyAudioSettings = [&]() {
|
|
|
|
|
|
if (!renderer) return;
|
2026-02-19 02:46:52 -08:00
|
|
|
|
float masterScale = soundMuted_ ? 0.0f : static_cast<float>(pendingMasterVolume) / 100.0f;
|
2026-02-23 07:51:10 -08:00
|
|
|
|
audio::AudioEngine::instance().setMasterVolume(masterScale);
|
2026-02-09 17:12:35 -08:00
|
|
|
|
if (auto* music = renderer->getMusicManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
music->setVolume(pendingMusicVolume);
|
2026-02-09 17:12:35 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* ambient = renderer->getAmbientSoundManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
ambient->setVolumeScale(pendingAmbientVolume / 100.0f);
|
2026-02-09 17:12:35 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* ui = renderer->getUiSoundManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
ui->setVolumeScale(pendingUiVolume / 100.0f);
|
2026-02-09 17:12:35 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* combat = renderer->getCombatSoundManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
combat->setVolumeScale(pendingCombatVolume / 100.0f);
|
2026-02-09 17:12:35 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* spell = renderer->getSpellSoundManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
spell->setVolumeScale(pendingSpellVolume / 100.0f);
|
2026-02-09 17:12:35 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* movement = renderer->getMovementSoundManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
movement->setVolumeScale(pendingMovementVolume / 100.0f);
|
2026-02-09 17:12:35 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* footstep = renderer->getFootstepManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
footstep->setVolumeScale(pendingFootstepVolume / 100.0f);
|
2026-02-09 17:12:35 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* npcVoice = renderer->getNpcVoiceManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
npcVoice->setVolumeScale(pendingNpcVoiceVolume / 100.0f);
|
2026-02-09 17:12:35 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* mount = renderer->getMountSoundManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
mount->setVolumeScale(pendingMountVolume / 100.0f);
|
2026-02-09 17:12:35 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* activity = renderer->getActivitySoundManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
activity->setVolumeScale(pendingActivityVolume / 100.0f);
|
2026-02-09 17:12:35 -08:00
|
|
|
|
}
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
};
|
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-02-09 17:12:35 -08:00
|
|
|
|
ImGui::Text("Master Volume");
|
|
|
|
|
|
if (ImGui::SliderInt("##MasterVolume", &pendingMasterVolume, 0, 100, "%d%%")) {
|
|
|
|
|
|
applyAudioSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
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-02-23 08:01:20 -08:00
|
|
|
|
if (ImGui::Checkbox("Enable WoWee Music", &pendingUseOriginalSoundtrack)) {
|
2026-02-17 16:26:49 -08:00
|
|
|
|
if (renderer) {
|
|
|
|
|
|
if (auto* zm = renderer->getZoneManager()) {
|
|
|
|
|
|
zm->setUseOriginalSoundtrack(pendingUseOriginalSoundtrack);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::IsItemHovered())
|
2026-02-23 08:01:20 -08:00
|
|
|
|
ImGui::SetTooltip("Include WoWee music tracks in zone music rotation");
|
2026-02-17 16:26:49 -08:00
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
2026-02-09 17:12:35 -08:00
|
|
|
|
ImGui::Text("Music");
|
|
|
|
|
|
if (ImGui::SliderInt("##MusicVolume", &pendingMusicVolume, 0, 100, "%d%%")) {
|
|
|
|
|
|
applyAudioSettings();
|
|
|
|
|
|
}
|
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-02-09 17:12:35 -08:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Text("Ambient Sounds");
|
|
|
|
|
|
if (ImGui::SliderInt("##AmbientVolume", &pendingAmbientVolume, 0, 100, "%d%%")) {
|
|
|
|
|
|
applyAudioSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::TextWrapped("Weather, zones, cities, emitters");
|
2026-02-05 17:32:21 -08:00
|
|
|
|
|
2026-02-09 17:12:35 -08:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Text("UI Sounds");
|
|
|
|
|
|
if (ImGui::SliderInt("##UiVolume", &pendingUiVolume, 0, 100, "%d%%")) {
|
|
|
|
|
|
applyAudioSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::TextWrapped("Buttons, loot, quest complete");
|
2026-02-05 17:40:15 -08:00
|
|
|
|
|
2026-02-09 17:12:35 -08:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Text("Combat Sounds");
|
|
|
|
|
|
if (ImGui::SliderInt("##CombatVolume", &pendingCombatVolume, 0, 100, "%d%%")) {
|
|
|
|
|
|
applyAudioSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::TextWrapped("Weapon swings, impacts, grunts");
|
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-02-09 17:12:35 -08:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Text("Spell Sounds");
|
|
|
|
|
|
if (ImGui::SliderInt("##SpellVolume", &pendingSpellVolume, 0, 100, "%d%%")) {
|
|
|
|
|
|
applyAudioSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::TextWrapped("Magic casting and impacts");
|
2026-02-05 17:40:15 -08:00
|
|
|
|
|
2026-02-09 17:12:35 -08:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Text("Movement Sounds");
|
|
|
|
|
|
if (ImGui::SliderInt("##MovementVolume", &pendingMovementVolume, 0, 100, "%d%%")) {
|
|
|
|
|
|
applyAudioSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::TextWrapped("Water splashes, jump/land");
|
2026-02-05 17:32:21 -08:00
|
|
|
|
|
2026-02-09 17:12:35 -08:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Text("Footsteps");
|
|
|
|
|
|
if (ImGui::SliderInt("##FootstepVolume", &pendingFootstepVolume, 0, 100, "%d%%")) {
|
|
|
|
|
|
applyAudioSettings();
|
|
|
|
|
|
}
|
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-02-09 17:12:35 -08:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Text("NPC Voices");
|
|
|
|
|
|
if (ImGui::SliderInt("##NpcVoiceVolume", &pendingNpcVoiceVolume, 0, 100, "%d%%")) {
|
|
|
|
|
|
applyAudioSettings();
|
|
|
|
|
|
}
|
2026-02-05 17:51:14 -08:00
|
|
|
|
|
2026-02-09 17:12:35 -08:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Text("Mount Sounds");
|
|
|
|
|
|
if (ImGui::SliderInt("##MountVolume", &pendingMountVolume, 0, 100, "%d%%")) {
|
|
|
|
|
|
applyAudioSettings();
|
|
|
|
|
|
}
|
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-02-09 17:12:35 -08:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Text("Activity Sounds");
|
|
|
|
|
|
if (ImGui::SliderInt("##ActivityVolume", &pendingActivityVolume, 0, 100, "%d%%")) {
|
|
|
|
|
|
applyAudioSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::TextWrapped("Swimming, eating, drinking");
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Button("Restore Audio Defaults", ImVec2(-1, 0))) {
|
|
|
|
|
|
pendingMasterVolume = 100;
|
|
|
|
|
|
pendingMusicVolume = kDefaultMusicVolume;
|
|
|
|
|
|
pendingAmbientVolume = 100;
|
|
|
|
|
|
pendingUiVolume = 100;
|
|
|
|
|
|
pendingCombatVolume = 100;
|
|
|
|
|
|
pendingSpellVolume = 100;
|
|
|
|
|
|
pendingMovementVolume = 100;
|
|
|
|
|
|
pendingFootstepVolume = 100;
|
|
|
|
|
|
pendingNpcVoiceVolume = 100;
|
|
|
|
|
|
pendingMountVolume = 100;
|
|
|
|
|
|
pendingActivityVolume = 100;
|
|
|
|
|
|
applyAudioSettings();
|
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// GAMEPLAY TAB
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
if (ImGui::BeginTabItem("Gameplay")) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Text("Controls");
|
|
|
|
|
|
ImGui::Separator();
|
2026-02-09 17:12:35 -08:00
|
|
|
|
if (ImGui::SliderFloat("Mouse Sensitivity", &pendingMouseSensitivity, 0.05f, 1.0f, "%.2f")) {
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
if (auto* cameraController = renderer->getCameraController()) {
|
|
|
|
|
|
cameraController->setMouseSensitivity(pendingMouseSensitivity);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::Checkbox("Invert Mouse", &pendingInvertMouse)) {
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
if (auto* cameraController = renderer->getCameraController()) {
|
|
|
|
|
|
cameraController->setInvertMouse(pendingInvertMouse);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
2026-02-23 08:09:27 -08:00
|
|
|
|
if (ImGui::Checkbox("Extended Camera Zoom", &pendingExtendedZoom)) {
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
if (auto* cameraController = renderer->getCameraController()) {
|
|
|
|
|
|
cameraController->setExtendedZoom(pendingExtendedZoom);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::IsItemHovered())
|
|
|
|
|
|
ImGui::SetTooltip("Allow the camera to zoom out further than normal");
|
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-11 22:13:22 -07:00
|
|
|
|
if (ImGui::SliderFloat("Field of View", &pendingFov, 45.0f, 110.0f, "%.0f°")) {
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
if (auto* camera = renderer->getCamera()) {
|
|
|
|
|
|
camera->setFov(pendingFov);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::IsItemHovered())
|
|
|
|
|
|
ImGui::SetTooltip("Camera field of view in degrees (default: 70)");
|
|
|
|
|
|
|
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
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Text("Interface");
|
|
|
|
|
|
ImGui::Separator();
|
2026-02-09 17:12:35 -08:00
|
|
|
|
if (ImGui::SliderInt("UI Opacity", &pendingUiOpacity, 20, 100, "%d%%")) {
|
|
|
|
|
|
uiOpacity_ = static_cast<float>(pendingUiOpacity) / 100.0f;
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::Checkbox("Rotate Minimap", &pendingMinimapRotate)) {
|
2026-02-11 17:30:57 -08:00
|
|
|
|
// Force north-up minimap.
|
|
|
|
|
|
minimapRotate_ = false;
|
|
|
|
|
|
pendingMinimapRotate = false;
|
2026-02-09 17:12:35 -08:00
|
|
|
|
if (renderer) {
|
|
|
|
|
|
if (auto* minimap = renderer->getMinimap()) {
|
2026-02-11 17:30:57 -08:00
|
|
|
|
minimap->setRotateWithCamera(false);
|
2026-02-09 17:12:35 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
2026-02-09 17:39:21 -08:00
|
|
|
|
if (ImGui::Checkbox("Square Minimap", &pendingMinimapSquare)) {
|
|
|
|
|
|
minimapSquare_ = pendingMinimapSquare;
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
if (auto* minimap = renderer->getMinimap()) {
|
|
|
|
|
|
minimap->setSquareShape(minimapSquare_);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
2026-02-20 16:40:22 -08:00
|
|
|
|
if (ImGui::Checkbox("Show Nearby NPC Dots", &pendingMinimapNpcDots)) {
|
|
|
|
|
|
minimapNpcDots_ = pendingMinimapNpcDots;
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
2026-02-09 17:39:21 -08:00
|
|
|
|
// Zoom controls
|
|
|
|
|
|
ImGui::Text("Minimap Zoom:");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button(" - ")) {
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
if (auto* minimap = renderer->getMinimap()) {
|
|
|
|
|
|
minimap->zoomOut();
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button(" + ")) {
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
if (auto* minimap = renderer->getMinimap()) {
|
|
|
|
|
|
minimap->zoomIn();
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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-02-17 16:31:00 -08:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Text("Loot");
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::Checkbox("Auto Loot", &pendingAutoLoot)) {
|
|
|
|
|
|
saveSettings(); // per-frame sync applies pendingAutoLoot to gameHandler
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::IsItemHovered())
|
|
|
|
|
|
ImGui::SetTooltip("Automatically pick up all items when looting");
|
2026-03-17 20:21:06 -07:00
|
|
|
|
if (ImGui::Checkbox("Auto Sell Greys", &pendingAutoSellGrey)) {
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::IsItemHovered())
|
|
|
|
|
|
ImGui::SetTooltip("Automatically sell all grey (poor quality) items when opening a vendor");
|
2026-03-17 20:27:45 -07:00
|
|
|
|
if (ImGui::Checkbox("Auto Repair", &pendingAutoRepair)) {
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::IsItemHovered())
|
|
|
|
|
|
ImGui::SetTooltip("Automatically repair all damaged equipment when opening an armorer vendor");
|
2026-02-17 16:31:00 -08:00
|
|
|
|
|
2026-02-13 22:51:49 -08:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Text("Bags");
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::Checkbox("Separate Bag Windows", &pendingSeparateBags)) {
|
|
|
|
|
|
inventoryScreen.setSeparateBags(pendingSeparateBags);
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
2026-03-17 08:18:46 -07:00
|
|
|
|
if (ImGui::Checkbox("Show Key Ring", &pendingShowKeyring)) {
|
|
|
|
|
|
inventoryScreen.setShowKeyring(pendingShowKeyring);
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
2026-02-13 22:51:49 -08:00
|
|
|
|
|
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
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Button("Restore Gameplay Defaults", ImVec2(-1, 0))) {
|
|
|
|
|
|
pendingMouseSensitivity = kDefaultMouseSensitivity;
|
|
|
|
|
|
pendingInvertMouse = kDefaultInvertMouse;
|
2026-02-23 08:09:27 -08:00
|
|
|
|
pendingExtendedZoom = false;
|
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
|
|
|
|
pendingUiOpacity = 65;
|
|
|
|
|
|
pendingMinimapRotate = false;
|
2026-02-09 17:39:21 -08:00
|
|
|
|
pendingMinimapSquare = false;
|
2026-02-20 16:40:22 -08:00
|
|
|
|
pendingMinimapNpcDots = false;
|
2026-02-13 22:51:49 -08:00
|
|
|
|
pendingSeparateBags = true;
|
|
|
|
|
|
inventoryScreen.setSeparateBags(true);
|
2026-03-17 08:18:46 -07:00
|
|
|
|
pendingShowKeyring = true;
|
|
|
|
|
|
inventoryScreen.setShowKeyring(true);
|
2026-02-09 17:12:35 -08:00
|
|
|
|
uiOpacity_ = 0.65f;
|
|
|
|
|
|
minimapRotate_ = false;
|
2026-02-09 17:39:21 -08:00
|
|
|
|
minimapSquare_ = false;
|
2026-02-20 16:40:22 -08:00
|
|
|
|
minimapNpcDots_ = false;
|
2026-02-09 17:12:35 -08:00
|
|
|
|
if (renderer) {
|
|
|
|
|
|
if (auto* cameraController = renderer->getCameraController()) {
|
|
|
|
|
|
cameraController->setMouseSensitivity(pendingMouseSensitivity);
|
|
|
|
|
|
cameraController->setInvertMouse(pendingInvertMouse);
|
2026-02-23 08:09:27 -08:00
|
|
|
|
cameraController->setExtendedZoom(pendingExtendedZoom);
|
2026-02-09 17:12:35 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* minimap = renderer->getMinimap()) {
|
|
|
|
|
|
minimap->setRotateWithCamera(minimapRotate_);
|
2026-02-09 17:39:21 -08:00
|
|
|
|
minimap->setSquareShape(minimapSquare_);
|
2026-02-09 17:12:35 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
saveSettings();
|
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-02-12 22:56:36 -08:00
|
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 06:51:48 -07:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// CONTROLS TAB
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
if (ImGui::BeginTabItem("Controls")) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Text("Keybindings");
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
auto& km = ui::KeybindingManager::getInstance();
|
|
|
|
|
|
int numActions = km.getActionCount();
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < numActions; ++i) {
|
|
|
|
|
|
auto action = static_cast<ui::KeybindingManager::Action>(i);
|
|
|
|
|
|
const char* actionName = km.getActionName(action);
|
|
|
|
|
|
ImGuiKey currentKey = km.getKeyForAction(action);
|
|
|
|
|
|
|
|
|
|
|
|
// Display current binding
|
|
|
|
|
|
ImGui::Text("%s:", actionName);
|
|
|
|
|
|
ImGui::SameLine(200);
|
|
|
|
|
|
|
|
|
|
|
|
// Get human-readable key name (basic implementation)
|
|
|
|
|
|
const char* keyName = "Unknown";
|
|
|
|
|
|
if (currentKey >= ImGuiKey_A && currentKey <= ImGuiKey_Z) {
|
|
|
|
|
|
static char keyBuf[16];
|
|
|
|
|
|
snprintf(keyBuf, sizeof(keyBuf), "%c", 'A' + (currentKey - ImGuiKey_A));
|
|
|
|
|
|
keyName = keyBuf;
|
|
|
|
|
|
} else if (currentKey >= ImGuiKey_0 && currentKey <= ImGuiKey_9) {
|
|
|
|
|
|
static char keyBuf[16];
|
|
|
|
|
|
snprintf(keyBuf, sizeof(keyBuf), "%c", '0' + (currentKey - ImGuiKey_0));
|
|
|
|
|
|
keyName = keyBuf;
|
|
|
|
|
|
} else if (currentKey == ImGuiKey_Escape) {
|
|
|
|
|
|
keyName = "Escape";
|
|
|
|
|
|
} else if (currentKey == ImGuiKey_Enter) {
|
|
|
|
|
|
keyName = "Enter";
|
|
|
|
|
|
} else if (currentKey == ImGuiKey_Tab) {
|
|
|
|
|
|
keyName = "Tab";
|
|
|
|
|
|
} else if (currentKey == ImGuiKey_Space) {
|
|
|
|
|
|
keyName = "Space";
|
|
|
|
|
|
} else if (currentKey >= ImGuiKey_F1 && currentKey <= ImGuiKey_F12) {
|
|
|
|
|
|
static char keyBuf[16];
|
|
|
|
|
|
snprintf(keyBuf, sizeof(keyBuf), "F%d", 1 + (currentKey - ImGuiKey_F1));
|
|
|
|
|
|
keyName = keyBuf;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Text("[%s]", keyName);
|
|
|
|
|
|
|
|
|
|
|
|
// Rebind button
|
|
|
|
|
|
ImGui::SameLine(350);
|
|
|
|
|
|
if (ImGui::Button(awaitingKeyPress && pendingRebindAction == i ? "Waiting..." : "Rebind", ImVec2(100, 0))) {
|
|
|
|
|
|
pendingRebindAction = i;
|
|
|
|
|
|
awaitingKeyPress = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Handle key press during rebinding
|
|
|
|
|
|
if (awaitingKeyPress && pendingRebindAction >= 0) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::Text("Press any key to bind to this action (Esc to cancel)...");
|
|
|
|
|
|
|
|
|
|
|
|
// Check for any key press
|
|
|
|
|
|
bool foundKey = false;
|
|
|
|
|
|
ImGuiKey newKey = ImGuiKey_None;
|
|
|
|
|
|
for (int k = ImGuiKey_NamedKey_BEGIN; k < ImGuiKey_NamedKey_END; ++k) {
|
|
|
|
|
|
if (ImGui::IsKeyPressed(static_cast<ImGuiKey>(k), false)) {
|
|
|
|
|
|
if (k == ImGuiKey_Escape) {
|
|
|
|
|
|
// Cancel rebinding
|
|
|
|
|
|
awaitingKeyPress = false;
|
|
|
|
|
|
pendingRebindAction = -1;
|
|
|
|
|
|
foundKey = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
newKey = static_cast<ImGuiKey>(k);
|
|
|
|
|
|
foundKey = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (foundKey && newKey != ImGuiKey_None) {
|
|
|
|
|
|
auto action = static_cast<ui::KeybindingManager::Action>(pendingRebindAction);
|
|
|
|
|
|
km.setKeyForAction(action, newKey);
|
|
|
|
|
|
awaitingKeyPress = false;
|
|
|
|
|
|
pendingRebindAction = -1;
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Button("Reset to Defaults", ImVec2(-1, 0))) {
|
|
|
|
|
|
km.resetToDefaults();
|
|
|
|
|
|
awaitingKeyPress = false;
|
|
|
|
|
|
pendingRebindAction = -1;
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 18:27:59 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// CHAT TAB
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
if (ImGui::BeginTabItem("Chat")) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Text("Appearance");
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Checkbox("Show Timestamps", &chatShowTimestamps_)) {
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SetItemTooltip("Show [HH:MM] before each chat message");
|
|
|
|
|
|
|
|
|
|
|
|
const char* fontSizes[] = { "Small", "Medium", "Large" };
|
|
|
|
|
|
if (ImGui::Combo("Chat Font Size", &chatFontSize_, fontSizes, 3)) {
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Text("Auto-Join Channels");
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Checkbox("General", &chatAutoJoinGeneral_)) saveSettings();
|
|
|
|
|
|
if (ImGui::Checkbox("Trade", &chatAutoJoinTrade_)) saveSettings();
|
|
|
|
|
|
if (ImGui::Checkbox("LocalDefense", &chatAutoJoinLocalDefense_)) saveSettings();
|
|
|
|
|
|
if (ImGui::Checkbox("LookingForGroup", &chatAutoJoinLFG_)) saveSettings();
|
2026-02-16 21:16:25 -08:00
|
|
|
|
if (ImGui::Checkbox("Local", &chatAutoJoinLocal_)) saveSettings();
|
2026-02-14 18:27:59 -08:00
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Text("Joined Channels");
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TextDisabled("Use /join and /leave commands in chat to manage channels.");
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Button("Restore Chat Defaults", ImVec2(-1, 0))) {
|
|
|
|
|
|
chatShowTimestamps_ = false;
|
|
|
|
|
|
chatFontSize_ = 1;
|
|
|
|
|
|
chatAutoJoinGeneral_ = true;
|
|
|
|
|
|
chatAutoJoinTrade_ = true;
|
|
|
|
|
|
chatAutoJoinLocalDefense_ = true;
|
|
|
|
|
|
chatAutoJoinLFG_ = true;
|
2026-02-16 21:16:25 -08:00
|
|
|
|
chatAutoJoinLocal_ = true;
|
2026-02-14 18:27:59 -08:00
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-23 07:51:10 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// ABOUT TAB
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
if (ImGui::BeginTabItem("About")) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TextWrapped("WoWee - World of Warcraft Client Emulator");
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Text("Developer");
|
|
|
|
|
|
ImGui::Indent();
|
|
|
|
|
|
ImGui::Text("Kelsi Davis");
|
|
|
|
|
|
ImGui::Unindent();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Text("GitHub");
|
|
|
|
|
|
ImGui::Indent();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.4f, 0.7f, 1.0f, 1.0f), "https://github.com/Kelsidavis/WoWee");
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
|
|
|
|
ImGui::SetTooltip("Click to copy");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::IsItemClicked()) {
|
|
|
|
|
|
ImGui::SetClipboardText("https://github.com/Kelsidavis/WoWee");
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Unindent();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Text("Contact");
|
|
|
|
|
|
ImGui::Indent();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.4f, 0.7f, 1.0f, 1.0f), "github.com/Kelsidavis");
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
|
|
|
|
ImGui::SetTooltip("Click to copy");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::IsItemClicked()) {
|
|
|
|
|
|
ImGui::SetClipboardText("https://github.com/Kelsidavis");
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Unindent();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TextWrapped("A multi-expansion WoW client supporting Classic, TBC, and WotLK (3.3.5a).");
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::TextDisabled("Built with Vulkan, SDL2, and ImGui");
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
ImGui::EndTabBar();
|
2026-02-06 20:19:39 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 16:16:03 -08:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 10.0f));
|
|
|
|
|
|
if (ImGui::Button("Back to Game", ImVec2(-1, 0))) {
|
2026-02-05 16:11:00 -08:00
|
|
|
|
showSettingsWindow = false;
|
|
|
|
|
|
}
|
2026-02-15 04:20:32 -08:00
|
|
|
|
ImGui::PopStyleVar();
|
2026-02-05 16:11:00 -08:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 15:21:48 -07:00
|
|
|
|
void GameScreen::applyGraphicsPreset(GraphicsPreset preset) {
|
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
|
|
|
|
|
|
// Define preset values based on quality level
|
|
|
|
|
|
switch (preset) {
|
|
|
|
|
|
case GraphicsPreset::LOW: {
|
|
|
|
|
|
pendingShadows = false;
|
|
|
|
|
|
pendingShadowDistance = 100.0f;
|
|
|
|
|
|
pendingAntiAliasing = 0; // Off
|
|
|
|
|
|
pendingNormalMapping = false;
|
|
|
|
|
|
pendingPOM = false;
|
|
|
|
|
|
pendingGroundClutterDensity = 25;
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
renderer->setShadowsEnabled(false);
|
|
|
|
|
|
renderer->setMsaaSamples(VK_SAMPLE_COUNT_1_BIT);
|
|
|
|
|
|
if (auto* wr = renderer->getWMORenderer()) {
|
|
|
|
|
|
wr->setNormalMappingEnabled(false);
|
|
|
|
|
|
wr->setPOMEnabled(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (auto* cr = renderer->getCharacterRenderer()) {
|
|
|
|
|
|
cr->setNormalMappingEnabled(false);
|
|
|
|
|
|
cr->setPOMEnabled(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (auto* tm = renderer->getTerrainManager()) {
|
|
|
|
|
|
tm->setGroundClutterDensityScale(0.25f);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case GraphicsPreset::MEDIUM: {
|
|
|
|
|
|
pendingShadows = true;
|
|
|
|
|
|
pendingShadowDistance = 200.0f;
|
|
|
|
|
|
pendingAntiAliasing = 1; // 2x MSAA
|
|
|
|
|
|
pendingNormalMapping = true;
|
|
|
|
|
|
pendingNormalMapStrength = 0.6f;
|
|
|
|
|
|
pendingPOM = true;
|
|
|
|
|
|
pendingPOMQuality = 0; // Low
|
|
|
|
|
|
pendingGroundClutterDensity = 60;
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
renderer->setShadowsEnabled(true);
|
|
|
|
|
|
renderer->setShadowDistance(200.0f);
|
|
|
|
|
|
renderer->setMsaaSamples(VK_SAMPLE_COUNT_2_BIT);
|
|
|
|
|
|
if (auto* wr = renderer->getWMORenderer()) {
|
|
|
|
|
|
wr->setNormalMappingEnabled(true);
|
|
|
|
|
|
wr->setNormalMapStrength(0.6f);
|
|
|
|
|
|
wr->setPOMEnabled(true);
|
|
|
|
|
|
wr->setPOMQuality(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (auto* cr = renderer->getCharacterRenderer()) {
|
|
|
|
|
|
cr->setNormalMappingEnabled(true);
|
|
|
|
|
|
cr->setNormalMapStrength(0.6f);
|
|
|
|
|
|
cr->setPOMEnabled(true);
|
|
|
|
|
|
cr->setPOMQuality(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (auto* tm = renderer->getTerrainManager()) {
|
|
|
|
|
|
tm->setGroundClutterDensityScale(0.60f);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case GraphicsPreset::HIGH: {
|
|
|
|
|
|
pendingShadows = true;
|
|
|
|
|
|
pendingShadowDistance = 350.0f;
|
|
|
|
|
|
pendingAntiAliasing = 2; // 4x MSAA
|
|
|
|
|
|
pendingNormalMapping = true;
|
|
|
|
|
|
pendingNormalMapStrength = 0.8f;
|
|
|
|
|
|
pendingPOM = true;
|
|
|
|
|
|
pendingPOMQuality = 1; // Medium
|
|
|
|
|
|
pendingGroundClutterDensity = 100;
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
renderer->setShadowsEnabled(true);
|
|
|
|
|
|
renderer->setShadowDistance(350.0f);
|
|
|
|
|
|
renderer->setMsaaSamples(VK_SAMPLE_COUNT_4_BIT);
|
|
|
|
|
|
if (auto* wr = renderer->getWMORenderer()) {
|
|
|
|
|
|
wr->setNormalMappingEnabled(true);
|
|
|
|
|
|
wr->setNormalMapStrength(0.8f);
|
|
|
|
|
|
wr->setPOMEnabled(true);
|
|
|
|
|
|
wr->setPOMQuality(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (auto* cr = renderer->getCharacterRenderer()) {
|
|
|
|
|
|
cr->setNormalMappingEnabled(true);
|
|
|
|
|
|
cr->setNormalMapStrength(0.8f);
|
|
|
|
|
|
cr->setPOMEnabled(true);
|
|
|
|
|
|
cr->setPOMQuality(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (auto* tm = renderer->getTerrainManager()) {
|
|
|
|
|
|
tm->setGroundClutterDensityScale(1.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case GraphicsPreset::ULTRA: {
|
|
|
|
|
|
pendingShadows = true;
|
|
|
|
|
|
pendingShadowDistance = 500.0f;
|
|
|
|
|
|
pendingAntiAliasing = 3; // 8x MSAA
|
2026-03-12 19:27:00 -07:00
|
|
|
|
pendingFXAA = true; // FXAA on top of MSAA for maximum smoothness
|
2026-03-11 15:21:48 -07:00
|
|
|
|
pendingNormalMapping = true;
|
|
|
|
|
|
pendingNormalMapStrength = 1.2f;
|
|
|
|
|
|
pendingPOM = true;
|
|
|
|
|
|
pendingPOMQuality = 2; // High
|
|
|
|
|
|
pendingGroundClutterDensity = 150;
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
renderer->setShadowsEnabled(true);
|
|
|
|
|
|
renderer->setShadowDistance(500.0f);
|
|
|
|
|
|
renderer->setMsaaSamples(VK_SAMPLE_COUNT_8_BIT);
|
2026-03-12 19:27:00 -07:00
|
|
|
|
renderer->setFXAAEnabled(true);
|
2026-03-11 15:21:48 -07:00
|
|
|
|
if (auto* wr = renderer->getWMORenderer()) {
|
|
|
|
|
|
wr->setNormalMappingEnabled(true);
|
|
|
|
|
|
wr->setNormalMapStrength(1.2f);
|
|
|
|
|
|
wr->setPOMEnabled(true);
|
|
|
|
|
|
wr->setPOMQuality(2);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (auto* cr = renderer->getCharacterRenderer()) {
|
|
|
|
|
|
cr->setNormalMappingEnabled(true);
|
|
|
|
|
|
cr->setNormalMapStrength(1.2f);
|
|
|
|
|
|
cr->setPOMEnabled(true);
|
|
|
|
|
|
cr->setPOMQuality(2);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (auto* tm = renderer->getTerrainManager()) {
|
|
|
|
|
|
tm->setGroundClutterDensityScale(1.5f);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
default:
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
currentGraphicsPreset = preset;
|
|
|
|
|
|
pendingGraphicsPreset = preset;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::updateGraphicsPresetFromCurrentSettings() {
|
|
|
|
|
|
// Check if current settings match any preset, otherwise mark as CUSTOM
|
|
|
|
|
|
// This is a simplified check; could be enhanced with more detailed matching
|
|
|
|
|
|
|
|
|
|
|
|
auto matchesPreset = [this](GraphicsPreset preset) -> bool {
|
|
|
|
|
|
switch (preset) {
|
|
|
|
|
|
case GraphicsPreset::LOW:
|
|
|
|
|
|
return !pendingShadows && pendingAntiAliasing == 0 && !pendingNormalMapping && !pendingPOM &&
|
|
|
|
|
|
pendingGroundClutterDensity <= 30;
|
|
|
|
|
|
case GraphicsPreset::MEDIUM:
|
|
|
|
|
|
return pendingShadows && pendingShadowDistance >= 180 && pendingShadowDistance <= 220 &&
|
|
|
|
|
|
pendingAntiAliasing == 1 && pendingNormalMapping && pendingPOM &&
|
|
|
|
|
|
pendingGroundClutterDensity >= 50 && pendingGroundClutterDensity <= 70;
|
|
|
|
|
|
case GraphicsPreset::HIGH:
|
|
|
|
|
|
return pendingShadows && pendingShadowDistance >= 330 && pendingShadowDistance <= 370 &&
|
|
|
|
|
|
pendingAntiAliasing == 2 && pendingNormalMapping && pendingPOM &&
|
|
|
|
|
|
pendingGroundClutterDensity >= 90 && pendingGroundClutterDensity <= 110;
|
|
|
|
|
|
case GraphicsPreset::ULTRA:
|
|
|
|
|
|
return pendingShadows && pendingShadowDistance >= 480 && pendingAntiAliasing == 3 &&
|
2026-03-12 19:27:00 -07:00
|
|
|
|
pendingFXAA && pendingNormalMapping && pendingPOM && pendingGroundClutterDensity >= 140;
|
2026-03-11 15:21:48 -07:00
|
|
|
|
default:
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Try to match a preset, otherwise mark as custom
|
|
|
|
|
|
if (matchesPreset(GraphicsPreset::LOW)) {
|
|
|
|
|
|
pendingGraphicsPreset = GraphicsPreset::LOW;
|
|
|
|
|
|
} else if (matchesPreset(GraphicsPreset::MEDIUM)) {
|
|
|
|
|
|
pendingGraphicsPreset = GraphicsPreset::MEDIUM;
|
|
|
|
|
|
} else if (matchesPreset(GraphicsPreset::HIGH)) {
|
|
|
|
|
|
pendingGraphicsPreset = GraphicsPreset::HIGH;
|
|
|
|
|
|
} else if (matchesPreset(GraphicsPreset::ULTRA)) {
|
|
|
|
|
|
pendingGraphicsPreset = GraphicsPreset::ULTRA;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
pendingGraphicsPreset = GraphicsPreset::CUSTOM;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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).
|
|
|
|
|
|
if (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.
|
|
|
|
|
|
if (minimapNpcDots_) {
|
|
|
|
|
|
const uint64_t selfGuid = gameHandler.getPlayerGuid();
|
|
|
|
|
|
const auto& partyData = gameHandler.getPartyData();
|
|
|
|
|
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
|
|
|
|
|
if (!entity || entity->getType() != game::ObjectType::PLAYER) continue;
|
|
|
|
|
|
if (entity->getGuid() == selfGuid) continue; // skip self (already drawn as arrow)
|
|
|
|
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
|
if (minimapNpcDots_) {
|
|
|
|
|
|
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
|
|
|
|
|
|
{
|
|
|
|
|
|
static const struct { const char* sym; ImU32 col; } kMMMarks[] = {
|
|
|
|
|
|
{ "\xe2\x98\x85", IM_COL32(255, 220, 50, 255) },
|
|
|
|
|
|
{ "\xe2\x97\x8f", IM_COL32(255, 140, 0, 255) },
|
|
|
|
|
|
{ "\xe2\x97\x86", IM_COL32(160, 32, 240, 255) },
|
|
|
|
|
|
{ "\xe2\x96\xb2", IM_COL32( 50, 200, 50, 255) },
|
|
|
|
|
|
{ "\xe2\x97\x8c", IM_COL32( 80, 160, 255, 255) },
|
|
|
|
|
|
{ "\xe2\x96\xa0", IM_COL32( 50, 200, 220, 255) },
|
|
|
|
|
|
{ "\xe2\x9c\x9d", IM_COL32(255, 80, 80, 255) },
|
|
|
|
|
|
{ "\xe2\x98\xa0", IM_COL32(255, 255, 255, 255) },
|
|
|
|
|
|
};
|
|
|
|
|
|
uint8_t pmk = gameHandler.getEntityRaidMark(member.guid);
|
|
|
|
|
|
if (pmk < game::GameHandler::kRaidMarkCount) {
|
|
|
|
|
|
ImFont* mmFont = ImGui::GetFont();
|
|
|
|
|
|
ImVec2 msz = mmFont->CalcTextSizeA(9.0f, FLT_MAX, 0.0f, kMMMarks[pmk].sym);
|
|
|
|
|
|
drawList->AddText(mmFont, 9.0f,
|
|
|
|
|
|
ImVec2(sx - msz.x * 0.5f, sy - 4.0f - msz.y),
|
|
|
|
|
|
kMMMarks[pmk].col, kMMMarks[pmk].sym);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-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) {
|
|
|
|
|
|
static const char* kMarkNames[] = {
|
|
|
|
|
|
"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-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);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.65f, 0.65f, 0.65f, 1.0f), "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")) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Minimap");
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool npcDots = minimapNpcDots_;
|
|
|
|
|
|
if (ImGui::MenuItem("Show NPC Dots", nullptr, npcDots)) {
|
|
|
|
|
|
minimapNpcDots_ = !minimapNpcDots_;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
float masterScale = soundMuted_ ? 0.0f : static_cast<float>(pendingMasterVolume) / 100.0f;
|
|
|
|
|
|
audio::AudioEngine::instance().setMasterVolume(masterScale);
|
|
|
|
|
|
if (!activeRenderer) return;
|
|
|
|
|
|
if (auto* music = activeRenderer->getMusicManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
music->setVolume(pendingMusicVolume);
|
2026-02-19 02:46:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* ambient = activeRenderer->getAmbientSoundManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
ambient->setVolumeScale(pendingAmbientVolume / 100.0f);
|
2026-02-19 02:46:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* ui = activeRenderer->getUiSoundManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
ui->setVolumeScale(pendingUiVolume / 100.0f);
|
2026-02-19 02:46:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* combat = activeRenderer->getCombatSoundManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
combat->setVolumeScale(pendingCombatVolume / 100.0f);
|
2026-02-19 02:46:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* spell = activeRenderer->getSpellSoundManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
spell->setVolumeScale(pendingSpellVolume / 100.0f);
|
2026-02-19 02:46:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* movement = activeRenderer->getMovementSoundManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
movement->setVolumeScale(pendingMovementVolume / 100.0f);
|
2026-02-19 02:46:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* footstep = activeRenderer->getFootstepManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
footstep->setVolumeScale(pendingFootstepVolume / 100.0f);
|
2026-02-19 02:46:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* npcVoice = activeRenderer->getNpcVoiceManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
npcVoice->setVolumeScale(pendingNpcVoiceVolume / 100.0f);
|
2026-02-19 02:46:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* mount = activeRenderer->getMountSoundManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
mount->setVolumeScale(pendingMountVolume / 100.0f);
|
2026-02-19 02:46:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* activity = activeRenderer->getActivitySoundManager()) {
|
2026-02-23 07:51:10 -08:00
|
|
|
|
activity->setVolumeScale(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-02-17 16:26:49 -08:00
|
|
|
|
soundMuted_ = !soundMuted_;
|
|
|
|
|
|
if (soundMuted_) {
|
2026-02-19 02:46:52 -08:00
|
|
|
|
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();
|
|
|
|
|
|
ImU32 bg = soundMuted_ ? IM_COL32(135, 42, 42, 230) : IM_COL32(38, 38, 38, 210);
|
|
|
|
|
|
if (hovered) bg = soundMuted_ ? IM_COL32(160, 58, 58, 230) : IM_COL32(65, 65, 65, 220);
|
|
|
|
|
|
ImU32 fg = IM_COL32(255, 255, 255, 245);
|
|
|
|
|
|
draw->AddRectFilled(p, ImVec2(p.x + size.x, p.y + size.y), bg, 4.0f);
|
|
|
|
|
|
draw->AddRect(ImVec2(p.x + 0.5f, p.y + 0.5f), ImVec2(p.x + size.x - 0.5f, p.y + size.y - 0.5f),
|
|
|
|
|
|
IM_COL32(255, 255, 255, 42), 4.0f);
|
|
|
|
|
|
draw->AddRectFilled(ImVec2(p.x + 4.0f, p.y + 8.0f), ImVec2(p.x + 7.0f, p.y + 12.0f), fg, 1.0f);
|
|
|
|
|
|
draw->AddTriangleFilled(ImVec2(p.x + 7.0f, p.y + 7.0f),
|
|
|
|
|
|
ImVec2(p.x + 7.0f, p.y + 13.0f),
|
|
|
|
|
|
ImVec2(p.x + 11.8f, p.y + 10.0f), fg);
|
|
|
|
|
|
if (soundMuted_) {
|
|
|
|
|
|
draw->AddLine(ImVec2(p.x + 13.5f, p.y + 6.2f), ImVec2(p.x + 17.2f, p.y + 13.8f), fg, 1.8f);
|
|
|
|
|
|
draw->AddLine(ImVec2(p.x + 17.2f, p.y + 6.2f), ImVec2(p.x + 13.5f, p.y + 13.8f), fg, 1.8f);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
draw->PathArcTo(ImVec2(p.x + 11.8f, p.y + 10.0f), 3.6f, -0.7f, 0.7f, 12);
|
|
|
|
|
|
draw->PathStroke(fg, 0, 1.4f);
|
|
|
|
|
|
draw->PathArcTo(ImVec2(p.x + 11.8f, p.y + 10.0f), 5.5f, -0.7f, 0.7f, 12);
|
|
|
|
|
|
draw->PathStroke(fg, 0, 1.2f);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (hovered) ImGui::SetTooltip(soundMuted_ ? "Unmute" : "Mute");
|
|
|
|
|
|
}
|
|
|
|
|
|
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-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-12 03:31:09 -07:00
|
|
|
|
// Latency indicator — centered at top of screen
|
2026-03-10 21:19:42 -07:00
|
|
|
|
uint32_t latMs = gameHandler.getLatencyMs();
|
2026-03-11 19:45:03 -07:00
|
|
|
|
if (showLatencyMeter_ && latMs > 0 && gameHandler.getState() == game::WorldState::IN_WORLD) {
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
char latBuf[32];
|
|
|
|
|
|
snprintf(latBuf, sizeof(latBuf), "%u ms", latMs);
|
|
|
|
|
|
ImVec2 textSize = ImGui::CalcTextSize(latBuf);
|
|
|
|
|
|
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-12 03:31:09 -07:00
|
|
|
|
ImGui::TextColored(latColor, "%s", latBuf);
|
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
|
|
|
|
std::string GameScreen::getSettingsPath() {
|
|
|
|
|
|
std::string dir;
|
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
|
const char* appdata = std::getenv("APPDATA");
|
|
|
|
|
|
dir = appdata ? std::string(appdata) + "\\wowee" : ".";
|
|
|
|
|
|
#else
|
|
|
|
|
|
const char* home = std::getenv("HOME");
|
|
|
|
|
|
dir = home ? std::string(home) + "/.wowee" : ".";
|
|
|
|
|
|
#endif
|
|
|
|
|
|
return dir + "/settings.cfg";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 17:39:21 -08:00
|
|
|
|
std::string GameScreen::replaceGenderPlaceholders(const std::string& text, game::GameHandler& gameHandler) {
|
2026-02-09 17:46:45 -08:00
|
|
|
|
// Get player gender, pronouns, and name
|
2026-02-09 17:39:21 -08:00
|
|
|
|
game::Gender gender = game::Gender::NONBINARY;
|
2026-02-09 17:46:45 -08:00
|
|
|
|
std::string playerName = "Adventurer";
|
2026-02-09 17:39:21 -08:00
|
|
|
|
const auto* character = gameHandler.getActiveCharacter();
|
|
|
|
|
|
if (character) {
|
|
|
|
|
|
gender = character->gender;
|
2026-02-09 17:46:45 -08:00
|
|
|
|
if (!character->name.empty()) {
|
|
|
|
|
|
playerName = character->name;
|
|
|
|
|
|
}
|
2026-02-09 17:39:21 -08:00
|
|
|
|
}
|
|
|
|
|
|
game::Pronouns pronouns = game::Pronouns::forGender(gender);
|
|
|
|
|
|
|
|
|
|
|
|
std::string result = text;
|
|
|
|
|
|
|
|
|
|
|
|
// Helper to trim whitespace
|
|
|
|
|
|
auto trim = [](std::string& s) {
|
2026-02-19 01:12:14 -08:00
|
|
|
|
const char* ws = " \t\n\r";
|
|
|
|
|
|
size_t start = s.find_first_not_of(ws);
|
|
|
|
|
|
if (start == std::string::npos) { s.clear(); return; }
|
|
|
|
|
|
size_t end = s.find_last_not_of(ws);
|
|
|
|
|
|
s = s.substr(start, end - start + 1);
|
2026-02-09 17:39:21 -08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-19 01:12:14 -08:00
|
|
|
|
// Replace $g/$G placeholders first.
|
2026-02-09 17:39:21 -08:00
|
|
|
|
size_t pos = 0;
|
|
|
|
|
|
while ((pos = result.find('$', pos)) != std::string::npos) {
|
|
|
|
|
|
if (pos + 1 >= result.length()) break;
|
2026-02-19 01:12:14 -08:00
|
|
|
|
char marker = result[pos + 1];
|
|
|
|
|
|
if (marker != 'g' && marker != 'G') { pos++; continue; }
|
2026-02-09 17:39:21 -08:00
|
|
|
|
|
|
|
|
|
|
size_t endPos = result.find(';', pos);
|
2026-02-19 01:12:14 -08:00
|
|
|
|
if (endPos == std::string::npos) { pos += 2; continue; }
|
2026-02-09 17:39:21 -08:00
|
|
|
|
|
|
|
|
|
|
std::string placeholder = result.substr(pos + 2, endPos - pos - 2);
|
|
|
|
|
|
|
|
|
|
|
|
// Split by colons
|
|
|
|
|
|
std::vector<std::string> parts;
|
|
|
|
|
|
size_t start = 0;
|
|
|
|
|
|
size_t colonPos;
|
|
|
|
|
|
while ((colonPos = placeholder.find(':', start)) != std::string::npos) {
|
|
|
|
|
|
std::string part = placeholder.substr(start, colonPos - start);
|
|
|
|
|
|
trim(part);
|
|
|
|
|
|
parts.push_back(part);
|
|
|
|
|
|
start = colonPos + 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Add the last part
|
|
|
|
|
|
std::string lastPart = placeholder.substr(start);
|
|
|
|
|
|
trim(lastPart);
|
|
|
|
|
|
parts.push_back(lastPart);
|
|
|
|
|
|
|
|
|
|
|
|
// Select appropriate text based on gender
|
|
|
|
|
|
std::string replacement;
|
|
|
|
|
|
if (parts.size() >= 3) {
|
|
|
|
|
|
// Three options: male, female, nonbinary
|
|
|
|
|
|
switch (gender) {
|
|
|
|
|
|
case game::Gender::MALE:
|
|
|
|
|
|
replacement = parts[0];
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::Gender::FEMALE:
|
|
|
|
|
|
replacement = parts[1];
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::Gender::NONBINARY:
|
|
|
|
|
|
replacement = parts[2];
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (parts.size() >= 2) {
|
|
|
|
|
|
// Two options: male, female (use first for nonbinary)
|
|
|
|
|
|
switch (gender) {
|
|
|
|
|
|
case game::Gender::MALE:
|
|
|
|
|
|
replacement = parts[0];
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::Gender::FEMALE:
|
|
|
|
|
|
replacement = parts[1];
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::Gender::NONBINARY:
|
|
|
|
|
|
// Default to gender-neutral: use the shorter/simpler option
|
|
|
|
|
|
replacement = parts[0].length() <= parts[1].length() ? parts[0] : parts[1];
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Malformed placeholder
|
|
|
|
|
|
pos = endPos + 1;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result.replace(pos, endPos - pos + 1, replacement);
|
|
|
|
|
|
pos += replacement.length();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-19 01:12:14 -08:00
|
|
|
|
// Replace simple placeholders.
|
|
|
|
|
|
// $n = player name
|
|
|
|
|
|
// $p = subject pronoun (he/she/they)
|
|
|
|
|
|
// $o = object pronoun (him/her/them)
|
|
|
|
|
|
// $s = possessive adjective (his/her/their)
|
|
|
|
|
|
// $S = possessive pronoun (his/hers/theirs)
|
|
|
|
|
|
// $b/$B = line break
|
|
|
|
|
|
pos = 0;
|
|
|
|
|
|
while ((pos = result.find('$', pos)) != std::string::npos) {
|
|
|
|
|
|
if (pos + 1 >= result.length()) break;
|
|
|
|
|
|
|
|
|
|
|
|
char code = result[pos + 1];
|
|
|
|
|
|
std::string replacement;
|
|
|
|
|
|
switch (code) {
|
|
|
|
|
|
case 'n': case 'N': replacement = playerName; break;
|
|
|
|
|
|
case 'p': replacement = pronouns.subject; break;
|
|
|
|
|
|
case 'o': replacement = pronouns.object; break;
|
|
|
|
|
|
case 's': replacement = pronouns.possessive; break;
|
|
|
|
|
|
case 'S': replacement = pronouns.possessiveP; break;
|
|
|
|
|
|
case 'b': case 'B': replacement = "\n"; break;
|
|
|
|
|
|
case 'g': case 'G': pos++; continue;
|
|
|
|
|
|
default: pos++; continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result.replace(pos, 2, replacement);
|
|
|
|
|
|
pos += replacement.length();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// WoW markup linebreak token.
|
|
|
|
|
|
pos = 0;
|
|
|
|
|
|
while ((pos = result.find("|n", pos)) != std::string::npos) {
|
|
|
|
|
|
result.replace(pos, 2, "\n");
|
|
|
|
|
|
pos += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
pos = 0;
|
|
|
|
|
|
while ((pos = result.find("|N", pos)) != std::string::npos) {
|
|
|
|
|
|
result.replace(pos, 2, "\n");
|
|
|
|
|
|
pos += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 17:39:21 -08:00
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 14:30:09 -08:00
|
|
|
|
void GameScreen::renderChatBubbles(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (chatBubbles_.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
|
|
|
|
|
if (!camera) 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;
|
|
|
|
|
|
|
|
|
|
|
|
// Get delta time from ImGui
|
|
|
|
|
|
float dt = ImGui::GetIO().DeltaTime;
|
|
|
|
|
|
|
|
|
|
|
|
glm::mat4 viewProj = camera->getProjectionMatrix() * camera->getViewMatrix();
|
|
|
|
|
|
|
|
|
|
|
|
// Update and render bubbles
|
|
|
|
|
|
for (int i = static_cast<int>(chatBubbles_.size()) - 1; i >= 0; --i) {
|
|
|
|
|
|
auto& bubble = chatBubbles_[i];
|
|
|
|
|
|
bubble.timeRemaining -= dt;
|
|
|
|
|
|
if (bubble.timeRemaining <= 0.0f) {
|
|
|
|
|
|
chatBubbles_.erase(chatBubbles_.begin() + i);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get entity position
|
|
|
|
|
|
auto entity = gameHandler.getEntityManager().getEntity(bubble.senderGuid);
|
|
|
|
|
|
if (!entity) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// Convert canonical → render coordinates, offset up by 2.5 units for bubble above head
|
|
|
|
|
|
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ() + 2.5f);
|
|
|
|
|
|
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
|
|
|
|
|
|
|
|
|
|
|
|
// Project to screen
|
|
|
|
|
|
glm::vec4 clipPos = viewProj * glm::vec4(renderPos, 1.0f);
|
|
|
|
|
|
if (clipPos.w <= 0.0f) continue; // Behind camera
|
|
|
|
|
|
|
|
|
|
|
|
glm::vec2 ndc(clipPos.x / clipPos.w, clipPos.y / clipPos.w);
|
|
|
|
|
|
float screenX = (ndc.x * 0.5f + 0.5f) * screenW;
|
2026-03-12 07:10:45 -07:00
|
|
|
|
// Camera bakes the Vulkan Y-flip into the projection matrix:
|
|
|
|
|
|
// NDC y=-1 is top, y=1 is bottom — same convention as nameplate/minimap projection.
|
|
|
|
|
|
float screenY = (ndc.y * 0.5f + 0.5f) * screenH;
|
2026-02-14 14:30:09 -08:00
|
|
|
|
|
|
|
|
|
|
// Skip if off-screen
|
|
|
|
|
|
if (screenX < -200.0f || screenX > screenW + 200.0f ||
|
|
|
|
|
|
screenY < -100.0f || screenY > screenH + 100.0f) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// Fade alpha over last 2 seconds
|
|
|
|
|
|
float alpha = 1.0f;
|
|
|
|
|
|
if (bubble.timeRemaining < 2.0f) {
|
|
|
|
|
|
alpha = bubble.timeRemaining / 2.0f;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Draw bubble window
|
|
|
|
|
|
std::string winId = "##ChatBubble" + std::to_string(bubble.senderGuid);
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenX, screenY), ImGuiCond_Always, ImVec2(0.5f, 1.0f));
|
|
|
|
|
|
ImGui::SetNextWindowBgAlpha(0.7f * alpha);
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
|
|
|
|
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
|
|
|
|
|
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoInputs |
|
|
|
|
|
|
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f);
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 4));
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Begin(winId.c_str(), nullptr, flags);
|
|
|
|
|
|
|
|
|
|
|
|
ImVec4 textColor = bubble.isYell
|
|
|
|
|
|
? ImVec4(1.0f, 0.2f, 0.2f, alpha)
|
|
|
|
|
|
: ImVec4(1.0f, 1.0f, 1.0f, alpha);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, textColor);
|
|
|
|
|
|
ImGui::PushTextWrapPos(200.0f);
|
|
|
|
|
|
ImGui::TextWrapped("%s", bubble.message.c_str());
|
|
|
|
|
|
ImGui::PopTextWrapPos();
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleVar(2);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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() {
|
|
|
|
|
|
std::string path = getSettingsPath();
|
|
|
|
|
|
std::filesystem::path dir = std::filesystem::path(path).parent_path();
|
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
|
std::filesystem::create_directories(dir, ec);
|
|
|
|
|
|
|
|
|
|
|
|
std::ofstream out(path);
|
|
|
|
|
|
if (!out.is_open()) {
|
|
|
|
|
|
LOG_WARNING("Could not save settings to ", path);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 17:39:21 -08:00
|
|
|
|
// Interface
|
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
|
|
|
|
out << "ui_opacity=" << pendingUiOpacity << "\n";
|
2026-02-07 20:51:53 -08:00
|
|
|
|
out << "minimap_rotate=" << (pendingMinimapRotate ? 1 : 0) << "\n";
|
2026-02-09 17:39:21 -08:00
|
|
|
|
out << "minimap_square=" << (pendingMinimapSquare ? 1 : 0) << "\n";
|
2026-02-20 16:40:22 -08:00
|
|
|
|
out << "minimap_npc_dots=" << (pendingMinimapNpcDots ? 1 : 0) << "\n";
|
2026-03-11 19:45:03 -07:00
|
|
|
|
out << "show_latency_meter=" << (pendingShowLatencyMeter ? 1 : 0) << "\n";
|
2026-03-12 04:04:27 -07:00
|
|
|
|
out << "show_dps_meter=" << (showDPSMeter_ ? 1 : 0) << "\n";
|
2026-03-12 15:25:07 -07:00
|
|
|
|
out << "show_cooldown_tracker=" << (showCooldownTracker_ ? 1 : 0) << "\n";
|
2026-02-13 22:51:49 -08:00
|
|
|
|
out << "separate_bags=" << (pendingSeparateBags ? 1 : 0) << "\n";
|
2026-03-17 08:18:46 -07:00
|
|
|
|
out << "show_keyring=" << (pendingShowKeyring ? 1 : 0) << "\n";
|
2026-03-11 22:39:59 -07:00
|
|
|
|
out << "action_bar_scale=" << pendingActionBarScale << "\n";
|
2026-03-11 22:49:54 -07:00
|
|
|
|
out << "nameplate_scale=" << nameplateScale_ << "\n";
|
2026-03-10 15:45:35 -07:00
|
|
|
|
out << "show_action_bar2=" << (pendingShowActionBar2 ? 1 : 0) << "\n";
|
|
|
|
|
|
out << "action_bar2_offset_x=" << pendingActionBar2OffsetX << "\n";
|
|
|
|
|
|
out << "action_bar2_offset_y=" << pendingActionBar2OffsetY << "\n";
|
2026-03-10 15:56:41 -07:00
|
|
|
|
out << "show_right_bar=" << (pendingShowRightBar ? 1 : 0) << "\n";
|
|
|
|
|
|
out << "show_left_bar=" << (pendingShowLeftBar ? 1 : 0) << "\n";
|
|
|
|
|
|
out << "right_bar_offset_y=" << pendingRightBarOffsetY << "\n";
|
|
|
|
|
|
out << "left_bar_offset_y=" << pendingLeftBarOffsetY << "\n";
|
2026-03-12 03:21:49 -07:00
|
|
|
|
out << "damage_flash=" << (damageFlashEnabled_ ? 1 : 0) << "\n";
|
2026-03-12 07:15:08 -07:00
|
|
|
|
out << "low_health_vignette=" << (lowHealthVignetteEnabled_ ? 1 : 0) << "\n";
|
2026-02-09 17:39:21 -08:00
|
|
|
|
|
|
|
|
|
|
// Audio
|
2026-02-17 16:26:49 -08:00
|
|
|
|
out << "sound_muted=" << (soundMuted_ ? 1 : 0) << "\n";
|
|
|
|
|
|
out << "use_original_soundtrack=" << (pendingUseOriginalSoundtrack ? 1 : 0) << "\n";
|
2026-02-09 17:39:21 -08:00
|
|
|
|
out << "master_volume=" << pendingMasterVolume << "\n";
|
|
|
|
|
|
out << "music_volume=" << pendingMusicVolume << "\n";
|
|
|
|
|
|
out << "ambient_volume=" << pendingAmbientVolume << "\n";
|
|
|
|
|
|
out << "ui_volume=" << pendingUiVolume << "\n";
|
|
|
|
|
|
out << "combat_volume=" << pendingCombatVolume << "\n";
|
|
|
|
|
|
out << "spell_volume=" << pendingSpellVolume << "\n";
|
|
|
|
|
|
out << "movement_volume=" << pendingMovementVolume << "\n";
|
|
|
|
|
|
out << "footstep_volume=" << pendingFootstepVolume << "\n";
|
|
|
|
|
|
out << "npc_voice_volume=" << pendingNpcVoiceVolume << "\n";
|
|
|
|
|
|
out << "mount_volume=" << pendingMountVolume << "\n";
|
|
|
|
|
|
out << "activity_volume=" << pendingActivityVolume << "\n";
|
|
|
|
|
|
|
2026-02-17 16:31:00 -08:00
|
|
|
|
// Gameplay
|
|
|
|
|
|
out << "auto_loot=" << (pendingAutoLoot ? 1 : 0) << "\n";
|
2026-03-17 20:21:06 -07:00
|
|
|
|
out << "auto_sell_grey=" << (pendingAutoSellGrey ? 1 : 0) << "\n";
|
2026-03-17 20:27:45 -07:00
|
|
|
|
out << "auto_repair=" << (pendingAutoRepair ? 1 : 0) << "\n";
|
2026-03-11 15:21:48 -07:00
|
|
|
|
out << "graphics_preset=" << static_cast<int>(currentGraphicsPreset) << "\n";
|
2026-02-21 01:26:16 -08:00
|
|
|
|
out << "ground_clutter_density=" << pendingGroundClutterDensity << "\n";
|
2026-02-23 08:40:16 -08:00
|
|
|
|
out << "shadows=" << (pendingShadows ? 1 : 0) << "\n";
|
2026-03-06 20:38:58 -08:00
|
|
|
|
out << "shadow_distance=" << pendingShadowDistance << "\n";
|
2026-03-17 09:04:53 -07:00
|
|
|
|
out << "brightness=" << pendingBrightness << "\n";
|
2026-03-06 19:15:34 -08:00
|
|
|
|
out << "water_refraction=" << (pendingWaterRefraction ? 1 : 0) << "\n";
|
2026-02-22 02:59:24 -08:00
|
|
|
|
out << "antialiasing=" << pendingAntiAliasing << "\n";
|
2026-03-12 16:43:48 -07:00
|
|
|
|
out << "fxaa=" << (pendingFXAA ? 1 : 0) << "\n";
|
2026-02-23 01:10:58 -08:00
|
|
|
|
out << "normal_mapping=" << (pendingNormalMapping ? 1 : 0) << "\n";
|
2026-02-23 01:18:42 -08:00
|
|
|
|
out << "normal_map_strength=" << pendingNormalMapStrength << "\n";
|
2026-02-23 01:10:58 -08:00
|
|
|
|
out << "pom=" << (pendingPOM ? 1 : 0) << "\n";
|
|
|
|
|
|
out << "pom_quality=" << pendingPOMQuality << "\n";
|
2026-03-08 20:22:11 -07:00
|
|
|
|
out << "upscaling_mode=" << pendingUpscalingMode << "\n";
|
2026-03-07 22:03:28 -08:00
|
|
|
|
out << "fsr=" << (pendingFSR ? 1 : 0) << "\n";
|
|
|
|
|
|
out << "fsr_quality=" << pendingFSRQuality << "\n";
|
|
|
|
|
|
out << "fsr_sharpness=" << pendingFSRSharpness << "\n";
|
2026-03-08 20:56:22 -07:00
|
|
|
|
out << "fsr2_jitter_sign=" << pendingFSR2JitterSign << "\n";
|
|
|
|
|
|
out << "fsr2_mv_scale_x=" << pendingFSR2MotionVecScaleX << "\n";
|
|
|
|
|
|
out << "fsr2_mv_scale_y=" << pendingFSR2MotionVecScaleY << "\n";
|
2026-03-08 22:53:21 -07:00
|
|
|
|
out << "amd_fsr3_framegen=" << (pendingAMDFramegen ? 1 : 0) << "\n";
|
2026-02-17 16:31:00 -08:00
|
|
|
|
|
2026-02-09 17:39:21 -08:00
|
|
|
|
// Controls
|
|
|
|
|
|
out << "mouse_sensitivity=" << pendingMouseSensitivity << "\n";
|
|
|
|
|
|
out << "invert_mouse=" << (pendingInvertMouse ? 1 : 0) << "\n";
|
2026-02-23 08:09:27 -08:00
|
|
|
|
out << "extended_zoom=" << (pendingExtendedZoom ? 1 : 0) << "\n";
|
2026-03-11 22:13:22 -07:00
|
|
|
|
out << "fov=" << 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
|
|
|
|
|
|
out << "chat_active_tab=" << activeChatTab_ << "\n";
|
2026-02-14 18:27:59 -08:00
|
|
|
|
out << "chat_timestamps=" << (chatShowTimestamps_ ? 1 : 0) << "\n";
|
|
|
|
|
|
out << "chat_font_size=" << chatFontSize_ << "\n";
|
|
|
|
|
|
out << "chat_autojoin_general=" << (chatAutoJoinGeneral_ ? 1 : 0) << "\n";
|
|
|
|
|
|
out << "chat_autojoin_trade=" << (chatAutoJoinTrade_ ? 1 : 0) << "\n";
|
|
|
|
|
|
out << "chat_autojoin_localdefense=" << (chatAutoJoinLocalDefense_ ? 1 : 0) << "\n";
|
|
|
|
|
|
out << "chat_autojoin_lfg=" << (chatAutoJoinLFG_ ? 1 : 0) << "\n";
|
2026-02-16 21:16:25 -08:00
|
|
|
|
out << "chat_autojoin_local=" << (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() {
|
|
|
|
|
|
std::string path = getSettingsPath();
|
|
|
|
|
|
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) {
|
|
|
|
|
|
pendingUiOpacity = v;
|
|
|
|
|
|
uiOpacity_ = static_cast<float>(v) / 100.0f;
|
|
|
|
|
|
}
|
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.
|
|
|
|
|
|
minimapRotate_ = false;
|
|
|
|
|
|
pendingMinimapRotate = false;
|
2026-02-09 17:39:21 -08:00
|
|
|
|
} else if (key == "minimap_square") {
|
|
|
|
|
|
int v = std::stoi(val);
|
|
|
|
|
|
minimapSquare_ = (v != 0);
|
|
|
|
|
|
pendingMinimapSquare = minimapSquare_;
|
2026-02-20 16:40:22 -08:00
|
|
|
|
} else if (key == "minimap_npc_dots") {
|
|
|
|
|
|
int v = std::stoi(val);
|
|
|
|
|
|
minimapNpcDots_ = (v != 0);
|
|
|
|
|
|
pendingMinimapNpcDots = minimapNpcDots_;
|
2026-03-11 19:45:03 -07:00
|
|
|
|
} else if (key == "show_latency_meter") {
|
|
|
|
|
|
showLatencyMeter_ = (std::stoi(val) != 0);
|
|
|
|
|
|
pendingShowLatencyMeter = showLatencyMeter_;
|
2026-03-12 04:04:27 -07:00
|
|
|
|
} else if (key == "show_dps_meter") {
|
|
|
|
|
|
showDPSMeter_ = (std::stoi(val) != 0);
|
2026-03-12 15:25:07 -07:00
|
|
|
|
} else if (key == "show_cooldown_tracker") {
|
|
|
|
|
|
showCooldownTracker_ = (std::stoi(val) != 0);
|
2026-02-13 22:51:49 -08:00
|
|
|
|
} else if (key == "separate_bags") {
|
|
|
|
|
|
pendingSeparateBags = (std::stoi(val) != 0);
|
|
|
|
|
|
inventoryScreen.setSeparateBags(pendingSeparateBags);
|
2026-03-17 08:18:46 -07:00
|
|
|
|
} else if (key == "show_keyring") {
|
|
|
|
|
|
pendingShowKeyring = (std::stoi(val) != 0);
|
|
|
|
|
|
inventoryScreen.setShowKeyring(pendingShowKeyring);
|
2026-03-11 22:39:59 -07:00
|
|
|
|
} else if (key == "action_bar_scale") {
|
|
|
|
|
|
pendingActionBarScale = std::clamp(std::stof(val), 0.5f, 1.5f);
|
2026-03-11 22:49:54 -07:00
|
|
|
|
} else if (key == "nameplate_scale") {
|
|
|
|
|
|
nameplateScale_ = std::clamp(std::stof(val), 0.5f, 2.0f);
|
2026-03-10 15:45:35 -07:00
|
|
|
|
} else if (key == "show_action_bar2") {
|
|
|
|
|
|
pendingShowActionBar2 = (std::stoi(val) != 0);
|
|
|
|
|
|
} else if (key == "action_bar2_offset_x") {
|
|
|
|
|
|
pendingActionBar2OffsetX = std::clamp(std::stof(val), -600.0f, 600.0f);
|
|
|
|
|
|
} else if (key == "action_bar2_offset_y") {
|
|
|
|
|
|
pendingActionBar2OffsetY = std::clamp(std::stof(val), -400.0f, 400.0f);
|
2026-03-10 15:56:41 -07:00
|
|
|
|
} else if (key == "show_right_bar") {
|
|
|
|
|
|
pendingShowRightBar = (std::stoi(val) != 0);
|
|
|
|
|
|
} else if (key == "show_left_bar") {
|
|
|
|
|
|
pendingShowLeftBar = (std::stoi(val) != 0);
|
|
|
|
|
|
} else if (key == "right_bar_offset_y") {
|
|
|
|
|
|
pendingRightBarOffsetY = std::clamp(std::stof(val), -400.0f, 400.0f);
|
|
|
|
|
|
} else if (key == "left_bar_offset_y") {
|
|
|
|
|
|
pendingLeftBarOffsetY = std::clamp(std::stof(val), -400.0f, 400.0f);
|
2026-03-12 03:21:49 -07:00
|
|
|
|
} else if (key == "damage_flash") {
|
|
|
|
|
|
damageFlashEnabled_ = (std::stoi(val) != 0);
|
2026-03-12 07:15:08 -07:00
|
|
|
|
} else if (key == "low_health_vignette") {
|
|
|
|
|
|
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") {
|
|
|
|
|
|
soundMuted_ = (std::stoi(val) != 0);
|
|
|
|
|
|
if (soundMuted_) {
|
|
|
|
|
|
// Apply mute on load; preMuteVolume_ will be set when AudioEngine is available
|
|
|
|
|
|
audio::AudioEngine::instance().setMasterVolume(0.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (key == "use_original_soundtrack") pendingUseOriginalSoundtrack = (std::stoi(val) != 0);
|
2026-02-09 17:39:21 -08:00
|
|
|
|
else if (key == "master_volume") pendingMasterVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
|
else if (key == "music_volume") pendingMusicVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
|
else if (key == "ambient_volume") pendingAmbientVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
|
else if (key == "ui_volume") pendingUiVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
|
else if (key == "combat_volume") pendingCombatVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
|
else if (key == "spell_volume") pendingSpellVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
|
else if (key == "movement_volume") pendingMovementVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
|
else if (key == "footstep_volume") pendingFootstepVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
|
else if (key == "npc_voice_volume") pendingNpcVoiceVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
|
else if (key == "mount_volume") pendingMountVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
|
else if (key == "activity_volume") pendingActivityVolume = std::clamp(std::stoi(val), 0, 100);
|
2026-02-17 16:31:00 -08:00
|
|
|
|
// Gameplay
|
|
|
|
|
|
else if (key == "auto_loot") pendingAutoLoot = (std::stoi(val) != 0);
|
2026-03-17 20:21:06 -07:00
|
|
|
|
else if (key == "auto_sell_grey") pendingAutoSellGrey = (std::stoi(val) != 0);
|
2026-03-17 20:27:45 -07:00
|
|
|
|
else if (key == "auto_repair") 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);
|
|
|
|
|
|
currentGraphicsPreset = static_cast<GraphicsPreset>(presetVal);
|
|
|
|
|
|
pendingGraphicsPreset = currentGraphicsPreset;
|
|
|
|
|
|
}
|
2026-02-21 01:26:16 -08:00
|
|
|
|
else if (key == "ground_clutter_density") pendingGroundClutterDensity = std::clamp(std::stoi(val), 0, 150);
|
2026-02-23 08:40:16 -08:00
|
|
|
|
else if (key == "shadows") pendingShadows = (std::stoi(val) != 0);
|
2026-03-07 22:29:06 -08:00
|
|
|
|
else if (key == "shadow_distance") pendingShadowDistance = std::clamp(std::stof(val), 40.0f, 500.0f);
|
2026-03-17 09:04:53 -07:00
|
|
|
|
else if (key == "brightness") {
|
|
|
|
|
|
pendingBrightness = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
|
if (auto* r = core::Application::getInstance().getRenderer())
|
|
|
|
|
|
r->setBrightness(static_cast<float>(pendingBrightness) / 50.0f);
|
|
|
|
|
|
}
|
2026-03-06 19:15:34 -08:00
|
|
|
|
else if (key == "water_refraction") pendingWaterRefraction = (std::stoi(val) != 0);
|
2026-02-22 02:59:24 -08:00
|
|
|
|
else if (key == "antialiasing") pendingAntiAliasing = std::clamp(std::stoi(val), 0, 3);
|
2026-03-12 16:43:48 -07:00
|
|
|
|
else if (key == "fxaa") pendingFXAA = (std::stoi(val) != 0);
|
2026-02-23 01:10:58 -08:00
|
|
|
|
else if (key == "normal_mapping") pendingNormalMapping = (std::stoi(val) != 0);
|
2026-02-23 01:18:42 -08:00
|
|
|
|
else if (key == "normal_map_strength") pendingNormalMapStrength = std::clamp(std::stof(val), 0.0f, 2.0f);
|
2026-02-23 01:10:58 -08:00
|
|
|
|
else if (key == "pom") pendingPOM = (std::stoi(val) != 0);
|
|
|
|
|
|
else if (key == "pom_quality") pendingPOMQuality = std::clamp(std::stoi(val), 0, 2);
|
2026-03-08 20:22:11 -07:00
|
|
|
|
else if (key == "upscaling_mode") {
|
|
|
|
|
|
pendingUpscalingMode = std::clamp(std::stoi(val), 0, 2);
|
|
|
|
|
|
pendingFSR = (pendingUpscalingMode == 1);
|
|
|
|
|
|
} else if (key == "fsr") {
|
|
|
|
|
|
pendingFSR = (std::stoi(val) != 0);
|
|
|
|
|
|
// Backward compatibility: old configs only had fsr=0/1.
|
|
|
|
|
|
if (pendingUpscalingMode == 0 && pendingFSR) pendingUpscalingMode = 1;
|
|
|
|
|
|
}
|
2026-03-07 22:03:28 -08:00
|
|
|
|
else if (key == "fsr_quality") pendingFSRQuality = std::clamp(std::stoi(val), 0, 3);
|
|
|
|
|
|
else if (key == "fsr_sharpness") pendingFSRSharpness = std::clamp(std::stof(val), 0.0f, 2.0f);
|
2026-03-08 20:56:22 -07:00
|
|
|
|
else if (key == "fsr2_jitter_sign") pendingFSR2JitterSign = std::clamp(std::stof(val), -2.0f, 2.0f);
|
|
|
|
|
|
else if (key == "fsr2_mv_scale_x") pendingFSR2MotionVecScaleX = std::clamp(std::stof(val), -2.0f, 2.0f);
|
|
|
|
|
|
else if (key == "fsr2_mv_scale_y") pendingFSR2MotionVecScaleY = std::clamp(std::stof(val), -2.0f, 2.0f);
|
2026-03-08 22:53:21 -07:00
|
|
|
|
else if (key == "amd_fsr3_framegen") pendingAMDFramegen = (std::stoi(val) != 0);
|
2026-02-09 17:39:21 -08:00
|
|
|
|
// Controls
|
|
|
|
|
|
else if (key == "mouse_sensitivity") pendingMouseSensitivity = std::clamp(std::stof(val), 0.05f, 1.0f);
|
|
|
|
|
|
else if (key == "invert_mouse") pendingInvertMouse = (std::stoi(val) != 0);
|
2026-02-23 08:09:27 -08:00
|
|
|
|
else if (key == "extended_zoom") pendingExtendedZoom = (std::stoi(val) != 0);
|
2026-03-11 22:13:22 -07:00
|
|
|
|
else if (key == "fov") {
|
|
|
|
|
|
pendingFov = std::clamp(std::stof(val), 45.0f, 110.0f);
|
|
|
|
|
|
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
|
|
|
|
|
if (auto* camera = renderer->getCamera()) camera->setFov(pendingFov);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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
|
|
|
|
|
|
else if (key == "chat_active_tab") activeChatTab_ = std::clamp(std::stoi(val), 0, 3);
|
2026-02-14 18:27:59 -08:00
|
|
|
|
else if (key == "chat_timestamps") chatShowTimestamps_ = (std::stoi(val) != 0);
|
|
|
|
|
|
else if (key == "chat_font_size") chatFontSize_ = std::clamp(std::stoi(val), 0, 2);
|
|
|
|
|
|
else if (key == "chat_autojoin_general") chatAutoJoinGeneral_ = (std::stoi(val) != 0);
|
|
|
|
|
|
else if (key == "chat_autojoin_trade") chatAutoJoinTrade_ = (std::stoi(val) != 0);
|
|
|
|
|
|
else if (key == "chat_autojoin_localdefense") chatAutoJoinLocalDefense_ = (std::stoi(val) != 0);
|
|
|
|
|
|
else if (key == "chat_autojoin_lfg") chatAutoJoinLFG_ = (std::stoi(val) != 0);
|
2026-02-16 21:16:25 -08:00
|
|
|
|
else if (key == "chat_autojoin_local") 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
|
|
|
|
|
|
uint64_t money = gameHandler.getMoneyCopper();
|
|
|
|
|
|
uint32_t mg = static_cast<uint32_t>(money / 10000);
|
|
|
|
|
|
uint32_t ms = static_cast<uint32_t>((money / 100) % 100);
|
|
|
|
|
|
uint32_t mc = static_cast<uint32_t>(money % 100);
|
2026-03-12 08:10:17 -07:00
|
|
|
|
ImGui::TextDisabled("Your money:"); ImGui::SameLine(0, 4);
|
|
|
|
|
|
renderCoinsText(mg, ms, mc);
|
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
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), " From: %s", mail.senderName.c_str());
|
|
|
|
|
|
if (mail.money > 0) {
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), " [G]");
|
|
|
|
|
|
}
|
|
|
|
|
|
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) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.2f, 0.2f, 1.0f), " [expires today!]");
|
|
|
|
|
|
} 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];
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "%s",
|
|
|
|
|
|
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) {
|
|
|
|
|
|
static const char* kMon[12] = {
|
|
|
|
|
|
"Jan","Feb","Mar","Apr","May","Jun",
|
|
|
|
|
|
"Jul","Aug","Sep","Oct","Nov","Dec"
|
|
|
|
|
|
};
|
|
|
|
|
|
const char* mname = kMon[tmExp->tm_mon];
|
|
|
|
|
|
int daysLeft = static_cast<int>(secsLeft / 86400.0f);
|
|
|
|
|
|
if (secsLeft <= 0.0f) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f),
|
|
|
|
|
|
"Expired: %s %d, %d", mname, tmExp->tm_mday, 1900 + tmExp->tm_year);
|
|
|
|
|
|
} else if (secsLeft < 3.0f * 86400.0f) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
|
|
|
|
|
|
"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) {
|
|
|
|
|
|
uint32_t g = mail.money / 10000;
|
|
|
|
|
|
uint32_t s = (mail.money / 100) % 100;
|
|
|
|
|
|
uint32_t c = mail.money % 100;
|
2026-03-12 08:15:46 -07:00
|
|
|
|
ImGui::TextDisabled("Money:"); ImGui::SameLine(0, 4);
|
|
|
|
|
|
renderCoinsText(g, s, c);
|
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) {
|
|
|
|
|
|
uint32_t g = mail.cod / 10000;
|
|
|
|
|
|
uint32_t s = (mail.cod / 100) % 100;
|
|
|
|
|
|
uint32_t c = mail.cod % 100;
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
|
|
|
|
|
|
"COD: %ug %us %uc (you pay this to take items)", g, s, c);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
size_t curLen = strlen(chatInputBuffer);
|
|
|
|
|
|
if (curLen + link.size() + 1 < sizeof(chatInputBuffer)) {
|
|
|
|
|
|
strncat(chatInputBuffer, link.c_str(), sizeof(chatInputBuffer) - curLen - 1);
|
|
|
|
|
|
chatInputMoveCursorToEnd = true;
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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);
|
|
|
|
|
|
size_t curLen = strlen(chatInputBuffer);
|
|
|
|
|
|
if (curLen + link.size() + 1 < sizeof(chatInputBuffer)) {
|
|
|
|
|
|
strncat(chatInputBuffer, link.c_str(), sizeof(chatInputBuffer) - curLen - 1);
|
|
|
|
|
|
chatInputMoveCursorToEnd = true;
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Right-click items in bags to attach");
|
|
|
|
|
|
|
|
|
|
|
|
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());
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Click to remove");
|
|
|
|
|
|
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");
|
|
|
|
|
|
|
|
|
|
|
|
uint32_t totalMoney = static_cast<uint32_t>(mailComposeMoney_[0]) * 10000 +
|
|
|
|
|
|
static_cast<uint32_t>(mailComposeMoney_[1]) * 100 +
|
|
|
|
|
|
static_cast<uint32_t>(mailComposeMoney_[2]);
|
|
|
|
|
|
|
2026-02-25 14:11:09 -08:00
|
|
|
|
uint32_t sendCost = attachCount > 0 ? static_cast<uint32_t>(30 * attachCount) : 30u;
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "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);
|
|
|
|
|
|
size_t curLen = strlen(chatInputBuffer);
|
|
|
|
|
|
if (curLen + link.size() + 1 < sizeof(chatInputBuffer)) {
|
|
|
|
|
|
strncat(chatInputBuffer, link.c_str(), sizeof(chatInputBuffer) - curLen - 1);
|
|
|
|
|
|
chatInputMoveCursorToEnd = true;
|
|
|
|
|
|
refocusChatInput = 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
|
|
|
|
}
|
|
|
|
|
|
}
|
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);
|
|
|
|
|
|
size_t curLen = strlen(chatInputBuffer);
|
|
|
|
|
|
if (curLen + link.size() + 1 < sizeof(chatInputBuffer)) {
|
|
|
|
|
|
strncat(chatInputBuffer, link.c_str(), sizeof(chatInputBuffer) - curLen - 1);
|
|
|
|
|
|
chatInputMoveCursorToEnd = true;
|
|
|
|
|
|
refocusChatInput = 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
|
|
|
|
}
|
|
|
|
|
|
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_),
|
|
|
|
|
|
q, getSearchClassId(), getSearchSubClassId(), 0, 0, offset);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 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")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
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);
|
|
|
|
|
|
size_t curLen = strlen(chatInputBuffer);
|
|
|
|
|
|
if (curLen + link.size() + 1 < sizeof(chatInputBuffer)) {
|
|
|
|
|
|
strncat(chatInputBuffer, link.c_str(), sizeof(chatInputBuffer) - curLen - 1);
|
|
|
|
|
|
chatInputMoveCursorToEnd = true;
|
|
|
|
|
|
refocusChatInput = 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
|
|
|
|
|
|
|
|
|
|
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-12 08:15:46 -07:00
|
|
|
|
renderCoinsText(bid / 10000, (bid / 100) % 100, bid % 100);
|
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-12 08:15:46 -07:00
|
|
|
|
renderCoinsText(auction.buyoutPrice / 10000,
|
|
|
|
|
|
(auction.buyoutPrice / 100) % 100, auction.buyoutPrice % 100);
|
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));
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
|
size_t curLen = strlen(chatInputBuffer);
|
|
|
|
|
|
if (curLen + link.size() + 1 < sizeof(chatInputBuffer)) {
|
|
|
|
|
|
strncat(chatInputBuffer, link.c_str(), sizeof(chatInputBuffer) - curLen - 1);
|
|
|
|
|
|
chatInputMoveCursorToEnd = true;
|
|
|
|
|
|
refocusChatInput = 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
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
|
|
|
|
|
ImGui::Text("%u", a.stackCount);
|
|
|
|
|
|
ImGui::TableSetColumnIndex(2);
|
2026-03-12 08:15:46 -07:00
|
|
|
|
renderCoinsText(a.currentBid / 10000, (a.currentBid / 100) % 100, a.currentBid % 100);
|
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-12 08:15:46 -07:00
|
|
|
|
renderCoinsText(a.buyoutPrice / 10000, (a.buyoutPrice / 100) % 100, a.buyoutPrice % 100);
|
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));
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
|
size_t curLen = strlen(chatInputBuffer);
|
|
|
|
|
|
if (curLen + link.size() + 1 < sizeof(chatInputBuffer)) {
|
|
|
|
|
|
strncat(chatInputBuffer, link.c_str(), sizeof(chatInputBuffer) - curLen - 1);
|
|
|
|
|
|
chatInputMoveCursorToEnd = true;
|
|
|
|
|
|
refocusChatInput = 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
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
|
|
|
|
|
ImGui::Text("%u", a.stackCount);
|
|
|
|
|
|
ImGui::TableSetColumnIndex(2);
|
|
|
|
|
|
{
|
|
|
|
|
|
uint32_t bid = a.currentBid > 0 ? a.currentBid : a.startBid;
|
2026-03-12 08:15:46 -07:00
|
|
|
|
renderCoinsText(bid / 10000, (bid / 100) % 100, bid % 100);
|
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-12 08:15:46 -07:00
|
|
|
|
renderCoinsText(a.buyoutPrice / 10000, (a.buyoutPrice / 100) % 100, a.buyoutPrice % 100);
|
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-02-17 17:23:42 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Level-Up Ding Animation
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
2026-03-12 17:54:49 -07:00
|
|
|
|
void GameScreen::triggerDing(uint32_t newLevel, uint32_t hpDelta, uint32_t manaDelta,
|
|
|
|
|
|
uint32_t str, uint32_t agi, uint32_t sta,
|
|
|
|
|
|
uint32_t intel, uint32_t spi) {
|
|
|
|
|
|
dingTimer_ = DING_DURATION;
|
|
|
|
|
|
dingLevel_ = newLevel;
|
|
|
|
|
|
dingHpDelta_ = hpDelta;
|
|
|
|
|
|
dingManaDelta_ = manaDelta;
|
|
|
|
|
|
dingStats_[0] = str;
|
|
|
|
|
|
dingStats_[1] = agi;
|
|
|
|
|
|
dingStats_[2] = sta;
|
|
|
|
|
|
dingStats_[3] = intel;
|
|
|
|
|
|
dingStats_[4] = spi;
|
2026-02-17 17:23:42 -08:00
|
|
|
|
|
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
if (auto* sfx = renderer->getUiSoundManager()) {
|
|
|
|
|
|
sfx->playLevelUp();
|
|
|
|
|
|
}
|
|
|
|
|
|
renderer->playEmote("cheer");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderDingEffect() {
|
|
|
|
|
|
if (dingTimer_ <= 0.0f) return;
|
|
|
|
|
|
|
|
|
|
|
|
float dt = ImGui::GetIO().DeltaTime;
|
|
|
|
|
|
dingTimer_ -= dt;
|
|
|
|
|
|
if (dingTimer_ < 0.0f) dingTimer_ = 0.0f;
|
|
|
|
|
|
|
2026-03-10 16:08:24 -07:00
|
|
|
|
// Show "You have reached level X!" for the first 2.5s, fade out over last 0.5s.
|
|
|
|
|
|
// The 3D visual effect is handled by Renderer::triggerLevelUpEffect (LevelUp.m2).
|
|
|
|
|
|
constexpr float kFadeTime = 0.5f;
|
|
|
|
|
|
float alpha = dingTimer_ < kFadeTime ? (dingTimer_ / kFadeTime) : 1.0f;
|
|
|
|
|
|
if (alpha <= 0.0f) return;
|
2026-02-17 17:23:42 -08:00
|
|
|
|
|
|
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
|
|
float cx = io.DisplaySize.x * 0.5f;
|
2026-03-10 16:08:24 -07:00
|
|
|
|
float cy = io.DisplaySize.y * 0.38f; // Upper-center, like WoW
|
2026-02-17 17:23:42 -08:00
|
|
|
|
|
|
|
|
|
|
ImDrawList* draw = ImGui::GetForegroundDrawList();
|
2026-03-10 16:08:24 -07:00
|
|
|
|
ImFont* font = ImGui::GetFont();
|
|
|
|
|
|
float baseSize = ImGui::GetFontSize();
|
|
|
|
|
|
float fontSize = baseSize * 1.8f;
|
|
|
|
|
|
|
|
|
|
|
|
char buf[64];
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "You have reached level %u!", dingLevel_);
|
|
|
|
|
|
|
|
|
|
|
|
ImVec2 sz = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, buf);
|
|
|
|
|
|
float tx = cx - sz.x * 0.5f;
|
|
|
|
|
|
float ty = cy - sz.y * 0.5f;
|
|
|
|
|
|
|
|
|
|
|
|
// Slight black outline for readability
|
|
|
|
|
|
draw->AddText(font, fontSize, ImVec2(tx + 2, ty + 2),
|
|
|
|
|
|
IM_COL32(0, 0, 0, (int)(alpha * 180)), buf);
|
|
|
|
|
|
// Gold text
|
|
|
|
|
|
draw->AddText(font, fontSize, ImVec2(tx, ty),
|
|
|
|
|
|
IM_COL32(255, 210, 0, (int)(alpha * 255)), buf);
|
2026-03-12 17:54:49 -07:00
|
|
|
|
|
|
|
|
|
|
// Stat gains below the main text (shown only if server sent deltas)
|
|
|
|
|
|
bool hasStatGains = (dingHpDelta_ > 0 || dingManaDelta_ > 0 ||
|
|
|
|
|
|
dingStats_[0] || dingStats_[1] || dingStats_[2] ||
|
|
|
|
|
|
dingStats_[3] || dingStats_[4]);
|
|
|
|
|
|
if (hasStatGains) {
|
|
|
|
|
|
float smallSize = baseSize * 0.95f;
|
|
|
|
|
|
float yOff = ty + sz.y + 6.0f;
|
|
|
|
|
|
|
|
|
|
|
|
// Build stat delta string: "+150 HP +80 Mana +2 Str +2 Agi ..."
|
|
|
|
|
|
static const char* kStatLabels[] = { "Str", "Agi", "Sta", "Int", "Spi" };
|
|
|
|
|
|
char statBuf[128];
|
|
|
|
|
|
int written = 0;
|
|
|
|
|
|
if (dingHpDelta_ > 0)
|
|
|
|
|
|
written += snprintf(statBuf + written, sizeof(statBuf) - written,
|
|
|
|
|
|
"+%u HP ", dingHpDelta_);
|
|
|
|
|
|
if (dingManaDelta_ > 0)
|
|
|
|
|
|
written += snprintf(statBuf + written, sizeof(statBuf) - written,
|
|
|
|
|
|
"+%u Mana ", dingManaDelta_);
|
|
|
|
|
|
for (int i = 0; i < 5 && written < (int)sizeof(statBuf) - 1; ++i) {
|
|
|
|
|
|
if (dingStats_[i] > 0)
|
|
|
|
|
|
written += snprintf(statBuf + written, sizeof(statBuf) - written,
|
|
|
|
|
|
"+%u %s ", dingStats_[i], kStatLabels[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Trim trailing spaces
|
|
|
|
|
|
while (written > 0 && statBuf[written - 1] == ' ') --written;
|
|
|
|
|
|
statBuf[written] = '\0';
|
|
|
|
|
|
|
|
|
|
|
|
if (written > 0) {
|
|
|
|
|
|
ImVec2 ssz = font->CalcTextSizeA(smallSize, FLT_MAX, 0.0f, statBuf);
|
|
|
|
|
|
float stx = cx - ssz.x * 0.5f;
|
|
|
|
|
|
draw->AddText(font, smallSize, ImVec2(stx + 1, yOff + 1),
|
|
|
|
|
|
IM_COL32(0, 0, 0, (int)(alpha * 160)), statBuf);
|
|
|
|
|
|
draw->AddText(font, smallSize, ImVec2(stx, yOff),
|
|
|
|
|
|
IM_COL32(100, 220, 100, (int)(alpha * 230)), statBuf);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-17 17:23:42 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 20:53:21 -07:00
|
|
|
|
void GameScreen::triggerAchievementToast(uint32_t achievementId, std::string name) {
|
2026-03-09 13:53:42 -07:00
|
|
|
|
achievementToastId_ = achievementId;
|
2026-03-10 20:53:21 -07:00
|
|
|
|
achievementToastName_ = std::move(name);
|
2026-03-09 13:53:42 -07:00
|
|
|
|
achievementToastTimer_ = ACHIEVEMENT_TOAST_DURATION;
|
|
|
|
|
|
|
|
|
|
|
|
// Play a UI sound if available
|
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
if (auto* sfx = renderer->getUiSoundManager()) {
|
|
|
|
|
|
sfx->playAchievementAlert();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderAchievementToast() {
|
|
|
|
|
|
if (achievementToastTimer_ <= 0.0f) return;
|
|
|
|
|
|
|
|
|
|
|
|
float dt = ImGui::GetIO().DeltaTime;
|
|
|
|
|
|
achievementToastTimer_ -= dt;
|
|
|
|
|
|
if (achievementToastTimer_ < 0.0f) achievementToastTimer_ = 0.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;
|
|
|
|
|
|
|
|
|
|
|
|
// Slide in from the right — fully visible for most of the duration, slides out at end
|
|
|
|
|
|
constexpr float SLIDE_TIME = 0.4f;
|
|
|
|
|
|
float slideIn = std::min(achievementToastTimer_, ACHIEVEMENT_TOAST_DURATION - achievementToastTimer_);
|
|
|
|
|
|
float slideFrac = (ACHIEVEMENT_TOAST_DURATION > 0.0f && SLIDE_TIME > 0.0f)
|
|
|
|
|
|
? std::min(slideIn / SLIDE_TIME, 1.0f)
|
|
|
|
|
|
: 1.0f;
|
|
|
|
|
|
|
|
|
|
|
|
constexpr float TOAST_W = 280.0f;
|
|
|
|
|
|
constexpr float TOAST_H = 60.0f;
|
|
|
|
|
|
float xFull = screenW - TOAST_W - 20.0f;
|
|
|
|
|
|
float xHidden = screenW + 10.0f;
|
|
|
|
|
|
float toastX = xHidden + (xFull - xHidden) * slideFrac;
|
|
|
|
|
|
float toastY = screenH - TOAST_H - 80.0f; // above action bar area
|
|
|
|
|
|
|
|
|
|
|
|
float alpha = std::min(1.0f, achievementToastTimer_ / 0.5f); // fade at very end
|
|
|
|
|
|
|
|
|
|
|
|
ImDrawList* draw = ImGui::GetForegroundDrawList();
|
|
|
|
|
|
|
|
|
|
|
|
// Background panel (gold border, dark fill)
|
|
|
|
|
|
ImVec2 tl(toastX, toastY);
|
|
|
|
|
|
ImVec2 br(toastX + TOAST_W, toastY + TOAST_H);
|
|
|
|
|
|
draw->AddRectFilled(tl, br, IM_COL32(30, 20, 10, (int)(alpha * 230)), 6.0f);
|
|
|
|
|
|
draw->AddRect(tl, br, IM_COL32(200, 170, 50, (int)(alpha * 255)), 6.0f, 0, 2.0f);
|
|
|
|
|
|
|
|
|
|
|
|
// Title
|
|
|
|
|
|
ImFont* font = ImGui::GetFont();
|
|
|
|
|
|
float titleSize = 14.0f;
|
|
|
|
|
|
float bodySize = 12.0f;
|
|
|
|
|
|
const char* title = "Achievement Earned!";
|
|
|
|
|
|
float titleW = font->CalcTextSizeA(titleSize, FLT_MAX, 0.0f, title).x;
|
|
|
|
|
|
float titleX = toastX + (TOAST_W - titleW) * 0.5f;
|
|
|
|
|
|
draw->AddText(font, titleSize, ImVec2(titleX + 1, toastY + 8 + 1),
|
|
|
|
|
|
IM_COL32(0, 0, 0, (int)(alpha * 180)), title);
|
|
|
|
|
|
draw->AddText(font, titleSize, ImVec2(titleX, toastY + 8),
|
|
|
|
|
|
IM_COL32(255, 215, 0, (int)(alpha * 255)), title);
|
|
|
|
|
|
|
2026-03-10 20:53:21 -07:00
|
|
|
|
// Achievement name (falls back to ID if name not available)
|
|
|
|
|
|
char idBuf[256];
|
|
|
|
|
|
const char* achText = achievementToastName_.empty()
|
|
|
|
|
|
? nullptr : achievementToastName_.c_str();
|
|
|
|
|
|
if (achText) {
|
|
|
|
|
|
std::snprintf(idBuf, sizeof(idBuf), "%s", achText);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
std::snprintf(idBuf, sizeof(idBuf), "Achievement #%u", achievementToastId_);
|
|
|
|
|
|
}
|
2026-03-09 13:53:42 -07:00
|
|
|
|
float idW = font->CalcTextSizeA(bodySize, FLT_MAX, 0.0f, idBuf).x;
|
|
|
|
|
|
float idX = toastX + (TOAST_W - idW) * 0.5f;
|
|
|
|
|
|
draw->AddText(font, bodySize, ImVec2(idX, toastY + 28),
|
|
|
|
|
|
IM_COL32(220, 200, 150, (int)(alpha * 255)), idBuf);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 15:42:55 -07:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Area discovery toast — "Discovered: <AreaName>! (+XP XP)" centered on screen
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderDiscoveryToast() {
|
|
|
|
|
|
if (discoveryToastTimer_ <= 0.0f) return;
|
|
|
|
|
|
|
|
|
|
|
|
float dt = ImGui::GetIO().DeltaTime;
|
|
|
|
|
|
discoveryToastTimer_ -= dt;
|
|
|
|
|
|
if (discoveryToastTimer_ < 0.0f) discoveryToastTimer_ = 0.0f;
|
|
|
|
|
|
|
|
|
|
|
|
// Fade: ramp up in first 0.4s, hold, fade out in last 1.0s
|
|
|
|
|
|
float alpha;
|
|
|
|
|
|
if (discoveryToastTimer_ > DISCOVERY_TOAST_DURATION - 0.4f)
|
|
|
|
|
|
alpha = 1.0f - (discoveryToastTimer_ - (DISCOVERY_TOAST_DURATION - 0.4f)) / 0.4f;
|
|
|
|
|
|
else if (discoveryToastTimer_ < 1.0f)
|
|
|
|
|
|
alpha = discoveryToastTimer_;
|
|
|
|
|
|
else
|
|
|
|
|
|
alpha = 1.0f;
|
|
|
|
|
|
alpha = std::clamp(alpha, 0.0f, 1.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;
|
|
|
|
|
|
|
|
|
|
|
|
ImFont* font = ImGui::GetFont();
|
|
|
|
|
|
ImDrawList* draw = ImGui::GetForegroundDrawList();
|
|
|
|
|
|
|
|
|
|
|
|
const char* header = "Discovered!";
|
|
|
|
|
|
float headerSize = 16.0f;
|
|
|
|
|
|
float nameSize = 28.0f;
|
|
|
|
|
|
float xpSize = 14.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImVec2 headerDim = font->CalcTextSizeA(headerSize, FLT_MAX, 0.0f, header);
|
|
|
|
|
|
ImVec2 nameDim = font->CalcTextSizeA(nameSize, FLT_MAX, 0.0f, discoveryToastName_.c_str());
|
|
|
|
|
|
|
|
|
|
|
|
char xpBuf[48];
|
|
|
|
|
|
if (discoveryToastXP_ > 0)
|
|
|
|
|
|
snprintf(xpBuf, sizeof(xpBuf), "+%u XP", discoveryToastXP_);
|
|
|
|
|
|
else
|
|
|
|
|
|
xpBuf[0] = '\0';
|
|
|
|
|
|
ImVec2 xpDim = font->CalcTextSizeA(xpSize, FLT_MAX, 0.0f, xpBuf);
|
|
|
|
|
|
|
|
|
|
|
|
// Position slightly below zone text (at 37% down screen)
|
|
|
|
|
|
float centreY = screenH * 0.37f;
|
|
|
|
|
|
float headerX = (screenW - headerDim.x) * 0.5f;
|
|
|
|
|
|
float nameX = (screenW - nameDim.x) * 0.5f;
|
|
|
|
|
|
float xpX = (screenW - xpDim.x) * 0.5f;
|
|
|
|
|
|
float headerY = centreY;
|
|
|
|
|
|
float nameY = centreY + headerDim.y + 4.0f;
|
|
|
|
|
|
float xpY = nameY + nameDim.y + 4.0f;
|
|
|
|
|
|
|
|
|
|
|
|
// "Discovered!" in gold
|
|
|
|
|
|
draw->AddText(font, headerSize, ImVec2(headerX + 1, headerY + 1),
|
|
|
|
|
|
IM_COL32(0, 0, 0, (int)(alpha * 160)), header);
|
|
|
|
|
|
draw->AddText(font, headerSize, ImVec2(headerX, headerY),
|
|
|
|
|
|
IM_COL32(255, 215, 0, (int)(alpha * 255)), header);
|
|
|
|
|
|
|
|
|
|
|
|
// Area name in white
|
|
|
|
|
|
draw->AddText(font, nameSize, ImVec2(nameX + 1, nameY + 1),
|
|
|
|
|
|
IM_COL32(0, 0, 0, (int)(alpha * 160)), discoveryToastName_.c_str());
|
|
|
|
|
|
draw->AddText(font, nameSize, ImVec2(nameX, nameY),
|
|
|
|
|
|
IM_COL32(255, 255, 255, (int)(alpha * 255)), discoveryToastName_.c_str());
|
|
|
|
|
|
|
|
|
|
|
|
// XP gain in light green (if any)
|
|
|
|
|
|
if (xpBuf[0] != '\0') {
|
|
|
|
|
|
draw->AddText(font, xpSize, ImVec2(xpX + 1, xpY + 1),
|
|
|
|
|
|
IM_COL32(0, 0, 0, (int)(alpha * 140)), xpBuf);
|
|
|
|
|
|
draw->AddText(font, xpSize, ImVec2(xpX, xpY),
|
|
|
|
|
|
IM_COL32(100, 220, 100, (int)(alpha * 230)), xpBuf);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 15:57:09 -07:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Quest objective progress toasts — shown at screen bottom-right on kill/item updates
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderQuestProgressToasts() {
|
|
|
|
|
|
if (questToasts_.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
float dt = ImGui::GetIO().DeltaTime;
|
|
|
|
|
|
for (auto& t : questToasts_) t.age += dt;
|
|
|
|
|
|
questToasts_.erase(
|
|
|
|
|
|
std::remove_if(questToasts_.begin(), questToasts_.end(),
|
|
|
|
|
|
[](const QuestProgressToastEntry& t) { return t.age >= QUEST_TOAST_DURATION; }),
|
|
|
|
|
|
questToasts_.end());
|
|
|
|
|
|
if (questToasts_.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
// Stack at bottom-right, just above action bar area
|
|
|
|
|
|
constexpr float TOAST_W = 240.0f;
|
|
|
|
|
|
constexpr float TOAST_H = 48.0f;
|
|
|
|
|
|
constexpr float TOAST_GAP = 4.0f;
|
|
|
|
|
|
float baseY = screenH * 0.72f;
|
|
|
|
|
|
float toastX = screenW - TOAST_W - 14.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImDrawList* bgDL = ImGui::GetBackgroundDrawList();
|
|
|
|
|
|
const int count = static_cast<int>(questToasts_.size());
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < count; ++i) {
|
|
|
|
|
|
const auto& toast = questToasts_[i];
|
|
|
|
|
|
|
|
|
|
|
|
float remaining = QUEST_TOAST_DURATION - toast.age;
|
|
|
|
|
|
float alpha;
|
|
|
|
|
|
if (toast.age < 0.2f)
|
|
|
|
|
|
alpha = toast.age / 0.2f;
|
|
|
|
|
|
else if (remaining < 1.0f)
|
|
|
|
|
|
alpha = remaining;
|
|
|
|
|
|
else
|
|
|
|
|
|
alpha = 1.0f;
|
|
|
|
|
|
alpha = std::clamp(alpha, 0.0f, 1.0f);
|
|
|
|
|
|
|
|
|
|
|
|
float ty = baseY - (count - i) * (TOAST_H + TOAST_GAP);
|
|
|
|
|
|
|
|
|
|
|
|
uint8_t bgA = static_cast<uint8_t>(200 * alpha);
|
|
|
|
|
|
uint8_t fgA = static_cast<uint8_t>(255 * alpha);
|
|
|
|
|
|
|
|
|
|
|
|
// Background: dark amber tint (quest color convention)
|
|
|
|
|
|
bgDL->AddRectFilled(ImVec2(toastX, ty), ImVec2(toastX + TOAST_W, ty + TOAST_H),
|
|
|
|
|
|
IM_COL32(35, 25, 5, bgA), 5.0f);
|
|
|
|
|
|
bgDL->AddRect(ImVec2(toastX, ty), ImVec2(toastX + TOAST_W, ty + TOAST_H),
|
|
|
|
|
|
IM_COL32(200, 160, 30, static_cast<uint8_t>(160 * alpha)), 5.0f, 0, 1.5f);
|
|
|
|
|
|
|
|
|
|
|
|
// Quest title (gold, small)
|
|
|
|
|
|
bgDL->AddText(ImVec2(toastX + 8.0f, ty + 5.0f),
|
|
|
|
|
|
IM_COL32(220, 180, 50, fgA), toast.questTitle.c_str());
|
|
|
|
|
|
|
|
|
|
|
|
// Progress bar + text: "ObjectiveName X / Y"
|
|
|
|
|
|
float barY = ty + 21.0f;
|
|
|
|
|
|
float barX0 = toastX + 8.0f;
|
|
|
|
|
|
float barX1 = toastX + TOAST_W - 8.0f;
|
|
|
|
|
|
float barH = 8.0f;
|
|
|
|
|
|
float pct = (toast.required > 0)
|
|
|
|
|
|
? std::min(1.0f, static_cast<float>(toast.current) / static_cast<float>(toast.required))
|
|
|
|
|
|
: 1.0f;
|
|
|
|
|
|
// Bar background
|
|
|
|
|
|
bgDL->AddRectFilled(ImVec2(barX0, barY), ImVec2(barX1, barY + barH),
|
|
|
|
|
|
IM_COL32(50, 40, 10, static_cast<uint8_t>(180 * alpha)), 3.0f);
|
|
|
|
|
|
// Bar fill — green when complete, amber otherwise
|
|
|
|
|
|
ImU32 barCol = (pct >= 1.0f) ? IM_COL32(60, 220, 80, fgA) : IM_COL32(200, 160, 30, fgA);
|
|
|
|
|
|
bgDL->AddRectFilled(ImVec2(barX0, barY),
|
|
|
|
|
|
ImVec2(barX0 + (barX1 - barX0) * pct, barY + barH),
|
|
|
|
|
|
barCol, 3.0f);
|
|
|
|
|
|
|
|
|
|
|
|
// Objective name + count
|
|
|
|
|
|
char progBuf[48];
|
|
|
|
|
|
if (!toast.objectiveName.empty())
|
|
|
|
|
|
snprintf(progBuf, sizeof(progBuf), "%.22s: %u/%u",
|
|
|
|
|
|
toast.objectiveName.c_str(), toast.current, toast.required);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(progBuf, sizeof(progBuf), "%u/%u", toast.current, toast.required);
|
|
|
|
|
|
bgDL->AddText(ImVec2(toastX + 8.0f, ty + 32.0f),
|
|
|
|
|
|
IM_COL32(220, 220, 200, static_cast<uint8_t>(210 * alpha)), progBuf);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 16:19:25 -07:00
|
|
|
|
// ---------------------------------------------------------------------------
|
2026-03-12 16:24:11 -07:00
|
|
|
|
// Item loot toasts — quality-coloured strip at bottom-left when item received
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderItemLootToasts() {
|
|
|
|
|
|
if (itemLootToasts_.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
float dt = ImGui::GetIO().DeltaTime;
|
|
|
|
|
|
for (auto& t : itemLootToasts_) t.age += dt;
|
|
|
|
|
|
itemLootToasts_.erase(
|
|
|
|
|
|
std::remove_if(itemLootToasts_.begin(), itemLootToasts_.end(),
|
|
|
|
|
|
[](const ItemLootToastEntry& t) { return t.age >= ITEM_LOOT_TOAST_DURATION; }),
|
|
|
|
|
|
itemLootToasts_.end());
|
|
|
|
|
|
if (itemLootToasts_.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
ImVec2 displaySize = ImGui::GetIO().DisplaySize;
|
|
|
|
|
|
float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f;
|
|
|
|
|
|
|
|
|
|
|
|
// Quality colours (matching WoW convention)
|
|
|
|
|
|
static const ImU32 kQualityColors[] = {
|
|
|
|
|
|
IM_COL32(157, 157, 157, 255), // 0 grey (poor)
|
|
|
|
|
|
IM_COL32(255, 255, 255, 255), // 1 white (common)
|
|
|
|
|
|
IM_COL32( 30, 255, 30, 255), // 2 green (uncommon)
|
|
|
|
|
|
IM_COL32( 0, 112, 221, 255), // 3 blue (rare)
|
|
|
|
|
|
IM_COL32(163, 53, 238, 255), // 4 purple (epic)
|
|
|
|
|
|
IM_COL32(255, 128, 0, 255), // 5 orange (legendary)
|
2026-03-13 10:29:56 -07:00
|
|
|
|
IM_COL32(230, 204, 128, 255), // 6 light gold (artifact)
|
|
|
|
|
|
IM_COL32(230, 204, 128, 255), // 7 light gold (heirloom)
|
2026-03-12 16:24:11 -07:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Stack at bottom-left above action bars; each item is 24 px tall
|
|
|
|
|
|
constexpr float TOAST_W = 260.0f;
|
|
|
|
|
|
constexpr float TOAST_H = 24.0f;
|
|
|
|
|
|
constexpr float TOAST_GAP = 2.0f;
|
|
|
|
|
|
constexpr float TOAST_X = 14.0f;
|
|
|
|
|
|
float baseY = screenH * 0.68f; // slightly above the whisper toasts
|
|
|
|
|
|
|
|
|
|
|
|
ImDrawList* bgDL = ImGui::GetBackgroundDrawList();
|
|
|
|
|
|
const int count = static_cast<int>(itemLootToasts_.size());
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < count; ++i) {
|
|
|
|
|
|
const auto& toast = itemLootToasts_[i];
|
|
|
|
|
|
|
|
|
|
|
|
float remaining = ITEM_LOOT_TOAST_DURATION - toast.age;
|
|
|
|
|
|
float alpha;
|
|
|
|
|
|
if (toast.age < 0.15f)
|
|
|
|
|
|
alpha = toast.age / 0.15f;
|
|
|
|
|
|
else if (remaining < 0.7f)
|
|
|
|
|
|
alpha = remaining / 0.7f;
|
|
|
|
|
|
else
|
|
|
|
|
|
alpha = 1.0f;
|
|
|
|
|
|
alpha = std::clamp(alpha, 0.0f, 1.0f);
|
|
|
|
|
|
|
|
|
|
|
|
// Slide-in from left
|
|
|
|
|
|
float slideX = (toast.age < 0.15f) ? (TOAST_W * (1.0f - toast.age / 0.15f)) : 0.0f;
|
|
|
|
|
|
float tx = TOAST_X - slideX;
|
|
|
|
|
|
float ty = baseY - (count - i) * (TOAST_H + TOAST_GAP);
|
|
|
|
|
|
|
|
|
|
|
|
uint8_t bgA = static_cast<uint8_t>(180 * alpha);
|
|
|
|
|
|
uint8_t fgA = static_cast<uint8_t>(255 * alpha);
|
|
|
|
|
|
|
|
|
|
|
|
// Background: very dark with quality-tinted left border accent
|
|
|
|
|
|
bgDL->AddRectFilled(ImVec2(tx, ty), ImVec2(tx + TOAST_W, ty + TOAST_H),
|
|
|
|
|
|
IM_COL32(12, 12, 12, bgA), 3.0f);
|
|
|
|
|
|
|
|
|
|
|
|
// Quality colour accent bar on left edge (3px wide)
|
2026-03-13 10:29:56 -07:00
|
|
|
|
ImU32 qualCol = kQualityColors[std::min(static_cast<uint32_t>(7u), toast.quality)];
|
2026-03-12 16:24:11 -07:00
|
|
|
|
ImU32 qualColA = (qualCol & 0x00FFFFFFu) | (static_cast<uint32_t>(fgA) << 24u);
|
|
|
|
|
|
bgDL->AddRectFilled(ImVec2(tx, ty), ImVec2(tx + 3.0f, ty + TOAST_H), qualColA, 3.0f);
|
|
|
|
|
|
|
|
|
|
|
|
// "Loot:" label in dim white
|
|
|
|
|
|
bgDL->AddText(ImVec2(tx + 7.0f, ty + 5.0f),
|
|
|
|
|
|
IM_COL32(160, 160, 160, static_cast<uint8_t>(200 * alpha)), "Loot:");
|
|
|
|
|
|
|
|
|
|
|
|
// Item name in quality colour
|
|
|
|
|
|
std::string displayName = toast.name.empty() ? ("Item #" + std::to_string(toast.itemId)) : toast.name;
|
|
|
|
|
|
if (displayName.size() > 26) { displayName.resize(23); displayName += "..."; }
|
|
|
|
|
|
bgDL->AddText(ImVec2(tx + 42.0f, ty + 5.0f), qualColA, displayName.c_str());
|
|
|
|
|
|
|
|
|
|
|
|
// Count (if > 1)
|
|
|
|
|
|
if (toast.count > 1) {
|
|
|
|
|
|
char countBuf[12];
|
|
|
|
|
|
snprintf(countBuf, sizeof(countBuf), "x%u", toast.count);
|
|
|
|
|
|
bgDL->AddText(ImVec2(tx + TOAST_W - 34.0f, ty + 5.0f),
|
|
|
|
|
|
IM_COL32(200, 200, 200, static_cast<uint8_t>(200 * alpha)), countBuf);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
2026-03-12 16:19:25 -07:00
|
|
|
|
// PvP honor credit toasts — shown at screen top-right on honorable kill
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderPvpHonorToasts() {
|
|
|
|
|
|
if (pvpHonorToasts_.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
float dt = ImGui::GetIO().DeltaTime;
|
|
|
|
|
|
for (auto& t : pvpHonorToasts_) t.age += dt;
|
|
|
|
|
|
pvpHonorToasts_.erase(
|
|
|
|
|
|
std::remove_if(pvpHonorToasts_.begin(), pvpHonorToasts_.end(),
|
|
|
|
|
|
[](const PvpHonorToastEntry& t) { return t.age >= PVP_HONOR_TOAST_DURATION; }),
|
|
|
|
|
|
pvpHonorToasts_.end());
|
|
|
|
|
|
if (pvpHonorToasts_.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
ImVec2 displaySize = ImGui::GetIO().DisplaySize;
|
|
|
|
|
|
float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f;
|
|
|
|
|
|
|
|
|
|
|
|
// Stack toasts at top-right, below any minimap area
|
|
|
|
|
|
constexpr float TOAST_W = 180.0f;
|
|
|
|
|
|
constexpr float TOAST_H = 30.0f;
|
|
|
|
|
|
constexpr float TOAST_GAP = 3.0f;
|
|
|
|
|
|
constexpr float TOAST_TOP = 10.0f;
|
|
|
|
|
|
float toastX = screenW - TOAST_W - 10.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImDrawList* bgDL = ImGui::GetBackgroundDrawList();
|
|
|
|
|
|
const int count = static_cast<int>(pvpHonorToasts_.size());
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < count; ++i) {
|
|
|
|
|
|
const auto& toast = pvpHonorToasts_[i];
|
|
|
|
|
|
|
|
|
|
|
|
float remaining = PVP_HONOR_TOAST_DURATION - toast.age;
|
|
|
|
|
|
float alpha;
|
|
|
|
|
|
if (toast.age < 0.15f)
|
|
|
|
|
|
alpha = toast.age / 0.15f;
|
|
|
|
|
|
else if (remaining < 0.8f)
|
|
|
|
|
|
alpha = remaining / 0.8f;
|
|
|
|
|
|
else
|
|
|
|
|
|
alpha = 1.0f;
|
|
|
|
|
|
alpha = std::clamp(alpha, 0.0f, 1.0f);
|
|
|
|
|
|
|
|
|
|
|
|
float ty = TOAST_TOP + i * (TOAST_H + TOAST_GAP);
|
|
|
|
|
|
|
|
|
|
|
|
uint8_t bgA = static_cast<uint8_t>(190 * alpha);
|
|
|
|
|
|
uint8_t fgA = static_cast<uint8_t>(255 * alpha);
|
|
|
|
|
|
|
|
|
|
|
|
// Background: dark red (PvP theme)
|
|
|
|
|
|
bgDL->AddRectFilled(ImVec2(toastX, ty), ImVec2(toastX + TOAST_W, ty + TOAST_H),
|
|
|
|
|
|
IM_COL32(28, 5, 5, bgA), 4.0f);
|
|
|
|
|
|
bgDL->AddRect(ImVec2(toastX, ty), ImVec2(toastX + TOAST_W, ty + TOAST_H),
|
|
|
|
|
|
IM_COL32(200, 50, 50, static_cast<uint8_t>(160 * alpha)), 4.0f, 0, 1.2f);
|
|
|
|
|
|
|
|
|
|
|
|
// Sword ⚔ icon (U+2694, UTF-8: e2 9a 94)
|
|
|
|
|
|
bgDL->AddText(ImVec2(toastX + 7.0f, ty + 7.0f),
|
|
|
|
|
|
IM_COL32(220, 80, 80, fgA), "\xe2\x9a\x94");
|
|
|
|
|
|
|
|
|
|
|
|
// "+N Honor" text in gold
|
|
|
|
|
|
char buf[40];
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "+%u Honor", toast.honor);
|
|
|
|
|
|
bgDL->AddText(ImVec2(toastX + 24.0f, ty + 8.0f),
|
|
|
|
|
|
IM_COL32(255, 210, 50, fgA), buf);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 16:12:21 -07:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Nearby player level-up toasts — shown at screen bottom-centre
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderPlayerLevelUpToasts(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (playerLevelUpToasts_.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
float dt = ImGui::GetIO().DeltaTime;
|
|
|
|
|
|
for (auto& t : playerLevelUpToasts_) {
|
|
|
|
|
|
t.age += dt;
|
|
|
|
|
|
// Lazy name resolution — fill in once the name cache has it
|
|
|
|
|
|
if (t.playerName.empty() && t.guid != 0) {
|
|
|
|
|
|
t.playerName = gameHandler.lookupName(t.guid);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
playerLevelUpToasts_.erase(
|
|
|
|
|
|
std::remove_if(playerLevelUpToasts_.begin(), playerLevelUpToasts_.end(),
|
|
|
|
|
|
[](const PlayerLevelUpToastEntry& t) {
|
|
|
|
|
|
return t.age >= PLAYER_LEVELUP_TOAST_DURATION;
|
|
|
|
|
|
}),
|
|
|
|
|
|
playerLevelUpToasts_.end());
|
|
|
|
|
|
if (playerLevelUpToasts_.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
// Stack toasts at screen bottom-centre, above action bars
|
|
|
|
|
|
constexpr float TOAST_W = 230.0f;
|
|
|
|
|
|
constexpr float TOAST_H = 38.0f;
|
|
|
|
|
|
constexpr float TOAST_GAP = 4.0f;
|
|
|
|
|
|
float baseY = screenH * 0.72f;
|
|
|
|
|
|
float toastX = (screenW - TOAST_W) * 0.5f;
|
|
|
|
|
|
|
|
|
|
|
|
ImDrawList* bgDL = ImGui::GetBackgroundDrawList();
|
|
|
|
|
|
const int count = static_cast<int>(playerLevelUpToasts_.size());
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < count; ++i) {
|
|
|
|
|
|
const auto& toast = playerLevelUpToasts_[i];
|
|
|
|
|
|
|
|
|
|
|
|
float remaining = PLAYER_LEVELUP_TOAST_DURATION - toast.age;
|
|
|
|
|
|
float alpha;
|
|
|
|
|
|
if (toast.age < 0.2f)
|
|
|
|
|
|
alpha = toast.age / 0.2f;
|
|
|
|
|
|
else if (remaining < 1.0f)
|
|
|
|
|
|
alpha = remaining;
|
|
|
|
|
|
else
|
|
|
|
|
|
alpha = 1.0f;
|
|
|
|
|
|
alpha = std::clamp(alpha, 0.0f, 1.0f);
|
|
|
|
|
|
|
|
|
|
|
|
// Subtle pop-up from below during first 0.2s
|
|
|
|
|
|
float slideY = (toast.age < 0.2f) ? (TOAST_H * (1.0f - toast.age / 0.2f)) : 0.0f;
|
|
|
|
|
|
float ty = baseY - (count - i) * (TOAST_H + TOAST_GAP) + slideY;
|
|
|
|
|
|
|
|
|
|
|
|
uint8_t bgA = static_cast<uint8_t>(200 * alpha);
|
|
|
|
|
|
uint8_t fgA = static_cast<uint8_t>(255 * alpha);
|
|
|
|
|
|
|
|
|
|
|
|
// Background: dark gold tint
|
|
|
|
|
|
bgDL->AddRectFilled(ImVec2(toastX, ty), ImVec2(toastX + TOAST_W, ty + TOAST_H),
|
|
|
|
|
|
IM_COL32(30, 22, 5, bgA), 5.0f);
|
|
|
|
|
|
// Gold border with glow at peak
|
|
|
|
|
|
float glowStr = (toast.age < 0.5f) ? (1.0f - toast.age / 0.5f) : 0.0f;
|
|
|
|
|
|
uint8_t borderA = static_cast<uint8_t>((160 + 80 * glowStr) * alpha);
|
|
|
|
|
|
bgDL->AddRect(ImVec2(toastX, ty), ImVec2(toastX + TOAST_W, ty + TOAST_H),
|
|
|
|
|
|
IM_COL32(255, 210, 50, borderA), 5.0f, 0, 1.5f + glowStr * 1.5f);
|
|
|
|
|
|
|
|
|
|
|
|
// Star ★ icon on left
|
|
|
|
|
|
bgDL->AddText(ImVec2(toastX + 8.0f, ty + 10.0f),
|
|
|
|
|
|
IM_COL32(255, 220, 60, fgA), "\xe2\x98\x85"); // UTF-8 ★
|
|
|
|
|
|
|
|
|
|
|
|
// "<Name> is now level X!" text
|
|
|
|
|
|
const char* displayName = toast.playerName.empty() ? "A player" : toast.playerName.c_str();
|
|
|
|
|
|
char buf[64];
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "%.18s is now level %u!", displayName, toast.newLevel);
|
|
|
|
|
|
bgDL->AddText(ImVec2(toastX + 26.0f, ty + 11.0f),
|
|
|
|
|
|
IM_COL32(255, 230, 100, fgA), buf);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 16:33:08 -07:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Resurrection flash — brief screen brightening + "You have been resurrected!"
|
|
|
|
|
|
// banner when the player transitions from ghost back to alive.
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderResurrectFlash() {
|
|
|
|
|
|
if (resurrectFlashTimer_ <= 0.0f) return;
|
|
|
|
|
|
|
|
|
|
|
|
float dt = ImGui::GetIO().DeltaTime;
|
|
|
|
|
|
resurrectFlashTimer_ -= dt;
|
|
|
|
|
|
if (resurrectFlashTimer_ <= 0.0f) {
|
|
|
|
|
|
resurrectFlashTimer_ = 0.0f;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
// Normalised age in [0, 1] (0 = just fired, 1 = fully elapsed)
|
|
|
|
|
|
float t = 1.0f - resurrectFlashTimer_ / kResurrectFlashDuration;
|
|
|
|
|
|
|
|
|
|
|
|
// Alpha envelope: fast fade-in (first 0.15s), hold, then fade-out (last 0.8s)
|
|
|
|
|
|
float alpha;
|
|
|
|
|
|
const float fadeIn = 0.15f / kResurrectFlashDuration; // ~5% of lifetime
|
|
|
|
|
|
const float fadeOut = 0.8f / kResurrectFlashDuration; // ~27% of lifetime
|
|
|
|
|
|
if (t < fadeIn)
|
|
|
|
|
|
alpha = t / fadeIn;
|
|
|
|
|
|
else if (t < 1.0f - fadeOut)
|
|
|
|
|
|
alpha = 1.0f;
|
|
|
|
|
|
else
|
|
|
|
|
|
alpha = (1.0f - t) / fadeOut;
|
|
|
|
|
|
alpha = std::clamp(alpha, 0.0f, 1.0f);
|
|
|
|
|
|
|
|
|
|
|
|
ImDrawList* bg = ImGui::GetBackgroundDrawList();
|
|
|
|
|
|
|
|
|
|
|
|
// Soft golden/white vignette — brightening instead of darkening
|
|
|
|
|
|
uint8_t vigA = static_cast<uint8_t>(50 * alpha);
|
|
|
|
|
|
bg->AddRectFilled(ImVec2(0, 0), ImVec2(screenW, screenH),
|
|
|
|
|
|
IM_COL32(200, 230, 255, vigA));
|
|
|
|
|
|
|
|
|
|
|
|
// Centered banner panel
|
|
|
|
|
|
constexpr float PANEL_W = 360.0f;
|
|
|
|
|
|
constexpr float PANEL_H = 52.0f;
|
|
|
|
|
|
float px = (screenW - PANEL_W) * 0.5f;
|
|
|
|
|
|
float py = screenH * 0.34f;
|
|
|
|
|
|
|
|
|
|
|
|
uint8_t bgA = static_cast<uint8_t>(210 * alpha);
|
|
|
|
|
|
uint8_t borderA = static_cast<uint8_t>(255 * alpha);
|
|
|
|
|
|
uint8_t textA = static_cast<uint8_t>(255 * alpha);
|
|
|
|
|
|
|
|
|
|
|
|
// Background: deep blue-black
|
|
|
|
|
|
bg->AddRectFilled(ImVec2(px, py), ImVec2(px + PANEL_W, py + PANEL_H),
|
|
|
|
|
|
IM_COL32(10, 18, 40, bgA), 8.0f);
|
|
|
|
|
|
|
|
|
|
|
|
// Border glow: bright holy gold
|
|
|
|
|
|
bg->AddRect(ImVec2(px, py), ImVec2(px + PANEL_W, py + PANEL_H),
|
|
|
|
|
|
IM_COL32(200, 230, 100, borderA), 8.0f, 0, 2.0f);
|
|
|
|
|
|
// Inner halo line
|
|
|
|
|
|
bg->AddRect(ImVec2(px + 3.0f, py + 3.0f), ImVec2(px + PANEL_W - 3.0f, py + PANEL_H - 3.0f),
|
|
|
|
|
|
IM_COL32(255, 255, 180, static_cast<uint8_t>(80 * alpha)), 6.0f, 0, 1.0f);
|
|
|
|
|
|
|
|
|
|
|
|
// "✦ You have been resurrected! ✦" centered
|
|
|
|
|
|
// UTF-8 heavy four-pointed star U+2726: \xe2\x9c\xa6
|
|
|
|
|
|
const char* banner = "\xe2\x9c\xa6 You have been resurrected! \xe2\x9c\xa6";
|
|
|
|
|
|
ImFont* font = ImGui::GetFont();
|
|
|
|
|
|
float fontSize = ImGui::GetFontSize();
|
|
|
|
|
|
ImVec2 textSz = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, banner);
|
|
|
|
|
|
float tx = px + (PANEL_W - textSz.x) * 0.5f;
|
|
|
|
|
|
float ty = py + (PANEL_H - textSz.y) * 0.5f;
|
|
|
|
|
|
|
|
|
|
|
|
// Drop shadow
|
|
|
|
|
|
bg->AddText(font, fontSize, ImVec2(tx + 1.0f, ty + 1.0f),
|
|
|
|
|
|
IM_COL32(0, 0, 0, static_cast<uint8_t>(180 * alpha)), banner);
|
|
|
|
|
|
// Main text in warm gold
|
|
|
|
|
|
bg->AddText(font, fontSize, ImVec2(tx, ty),
|
|
|
|
|
|
IM_COL32(255, 240, 120, textA), banner);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 17:06:12 -07:00
|
|
|
|
// ---------------------------------------------------------------------------
|
2026-03-12 15:53:45 -07:00
|
|
|
|
// Whisper toast notifications — brief overlay when a player whispers you
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderWhisperToasts() {
|
|
|
|
|
|
if (whisperToasts_.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
float dt = ImGui::GetIO().DeltaTime;
|
|
|
|
|
|
|
|
|
|
|
|
// Age and prune expired toasts
|
|
|
|
|
|
for (auto& t : whisperToasts_) t.age += dt;
|
|
|
|
|
|
whisperToasts_.erase(
|
|
|
|
|
|
std::remove_if(whisperToasts_.begin(), whisperToasts_.end(),
|
|
|
|
|
|
[](const WhisperToastEntry& t) { return t.age >= WHISPER_TOAST_DURATION; }),
|
|
|
|
|
|
whisperToasts_.end());
|
|
|
|
|
|
if (whisperToasts_.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
ImVec2 displaySize = ImGui::GetIO().DisplaySize;
|
|
|
|
|
|
float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f;
|
|
|
|
|
|
|
|
|
|
|
|
// Stack toasts at bottom-left, above the action bars (y ≈ screenH * 0.72)
|
|
|
|
|
|
// Each toast is ~56px tall with a 4px gap between them.
|
|
|
|
|
|
constexpr float TOAST_W = 280.0f;
|
|
|
|
|
|
constexpr float TOAST_H = 56.0f;
|
|
|
|
|
|
constexpr float TOAST_GAP = 4.0f;
|
|
|
|
|
|
constexpr float TOAST_X = 14.0f; // left edge (won't cover action bars)
|
|
|
|
|
|
float baseY = screenH * 0.72f;
|
|
|
|
|
|
|
|
|
|
|
|
ImDrawList* bgDL = ImGui::GetBackgroundDrawList();
|
|
|
|
|
|
|
|
|
|
|
|
const int count = static_cast<int>(whisperToasts_.size());
|
|
|
|
|
|
for (int i = 0; i < count; ++i) {
|
|
|
|
|
|
auto& toast = whisperToasts_[i];
|
|
|
|
|
|
|
|
|
|
|
|
// Fade in over 0.25s; fade out in last 1.0s
|
|
|
|
|
|
float alpha;
|
|
|
|
|
|
float remaining = WHISPER_TOAST_DURATION - toast.age;
|
|
|
|
|
|
if (toast.age < 0.25f)
|
|
|
|
|
|
alpha = toast.age / 0.25f;
|
|
|
|
|
|
else if (remaining < 1.0f)
|
|
|
|
|
|
alpha = remaining;
|
|
|
|
|
|
else
|
|
|
|
|
|
alpha = 1.0f;
|
|
|
|
|
|
alpha = std::clamp(alpha, 0.0f, 1.0f);
|
|
|
|
|
|
|
|
|
|
|
|
// Slide-in from left: offset 0→0 after 0.25s
|
|
|
|
|
|
float slideX = (toast.age < 0.25f) ? (TOAST_W * (1.0f - toast.age / 0.25f)) : 0.0f;
|
|
|
|
|
|
float tx = TOAST_X - slideX;
|
|
|
|
|
|
float ty = baseY - (count - i) * (TOAST_H + TOAST_GAP);
|
|
|
|
|
|
|
|
|
|
|
|
uint8_t bgA = static_cast<uint8_t>(210 * alpha);
|
|
|
|
|
|
uint8_t fgA = static_cast<uint8_t>(255 * alpha);
|
|
|
|
|
|
|
|
|
|
|
|
// Background panel — dark purple tint (whisper color convention)
|
|
|
|
|
|
bgDL->AddRectFilled(ImVec2(tx, ty), ImVec2(tx + TOAST_W, ty + TOAST_H),
|
|
|
|
|
|
IM_COL32(25, 10, 40, bgA), 6.0f);
|
|
|
|
|
|
// Purple border
|
|
|
|
|
|
bgDL->AddRect(ImVec2(tx, ty), ImVec2(tx + TOAST_W, ty + TOAST_H),
|
|
|
|
|
|
IM_COL32(160, 80, 220, static_cast<uint8_t>(180 * alpha)), 6.0f, 0, 1.5f);
|
|
|
|
|
|
|
|
|
|
|
|
// "Whisper" label (small, purple-ish)
|
|
|
|
|
|
bgDL->AddText(ImVec2(tx + 10.0f, ty + 6.0f),
|
|
|
|
|
|
IM_COL32(190, 110, 255, fgA), "Whisper from:");
|
|
|
|
|
|
|
|
|
|
|
|
// Sender name (gold)
|
|
|
|
|
|
bgDL->AddText(ImVec2(tx + 10.0f, ty + 20.0f),
|
|
|
|
|
|
IM_COL32(255, 210, 50, fgA), toast.sender.c_str());
|
|
|
|
|
|
|
|
|
|
|
|
// Message preview (white, dimmer)
|
|
|
|
|
|
bgDL->AddText(ImVec2(tx + 10.0f, ty + 36.0f),
|
|
|
|
|
|
IM_COL32(220, 220, 220, static_cast<uint8_t>(200 * alpha)),
|
|
|
|
|
|
toast.preview.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 17:06:12 -07:00
|
|
|
|
// Zone discovery text — "Entering: <ZoneName>" fades in/out at screen centre
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
2026-03-17 19:14:17 -07:00
|
|
|
|
void GameScreen::renderZoneText(game::GameHandler& gameHandler) {
|
|
|
|
|
|
// Poll worldStateZoneId for server-driven zone changes (fires on every zone crossing,
|
|
|
|
|
|
// including sub-zones like Ironforge within Dun Morogh).
|
|
|
|
|
|
uint32_t wsZoneId = gameHandler.getWorldStateZoneId();
|
|
|
|
|
|
if (wsZoneId != 0 && wsZoneId != lastKnownWorldStateZoneId_) {
|
|
|
|
|
|
lastKnownWorldStateZoneId_ = wsZoneId;
|
|
|
|
|
|
std::string wsName = gameHandler.getWhoAreaName(wsZoneId);
|
|
|
|
|
|
if (!wsName.empty()) {
|
|
|
|
|
|
zoneTextName_ = wsName;
|
|
|
|
|
|
zoneTextTimer_ = ZONE_TEXT_DURATION;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Also poll the renderer for zone name changes (covers map-level transitions
|
|
|
|
|
|
// where worldStateZoneId may not change immediately).
|
2026-03-09 17:06:12 -07:00
|
|
|
|
auto* appRenderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
if (appRenderer) {
|
|
|
|
|
|
const std::string& zoneName = appRenderer->getCurrentZoneName();
|
|
|
|
|
|
if (!zoneName.empty() && zoneName != lastKnownZoneName_) {
|
|
|
|
|
|
lastKnownZoneName_ = zoneName;
|
2026-03-17 19:14:17 -07:00
|
|
|
|
// Only override if the worldState hasn't already queued this zone
|
|
|
|
|
|
if (zoneTextName_ != zoneName) {
|
|
|
|
|
|
zoneTextName_ = zoneName;
|
|
|
|
|
|
zoneTextTimer_ = ZONE_TEXT_DURATION;
|
|
|
|
|
|
}
|
2026-03-09 17:06:12 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (zoneTextTimer_ <= 0.0f || zoneTextName_.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
float dt = ImGui::GetIO().DeltaTime;
|
|
|
|
|
|
zoneTextTimer_ -= dt;
|
|
|
|
|
|
if (zoneTextTimer_ < 0.0f) zoneTextTimer_ = 0.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;
|
|
|
|
|
|
|
|
|
|
|
|
// Fade: ramp up in first 0.5 s, hold, fade out in last 1.0 s
|
|
|
|
|
|
float alpha;
|
|
|
|
|
|
if (zoneTextTimer_ > ZONE_TEXT_DURATION - 0.5f)
|
|
|
|
|
|
alpha = 1.0f - (zoneTextTimer_ - (ZONE_TEXT_DURATION - 0.5f)) / 0.5f;
|
|
|
|
|
|
else if (zoneTextTimer_ < 1.0f)
|
|
|
|
|
|
alpha = zoneTextTimer_;
|
|
|
|
|
|
else
|
|
|
|
|
|
alpha = 1.0f;
|
|
|
|
|
|
alpha = std::clamp(alpha, 0.0f, 1.0f);
|
|
|
|
|
|
|
|
|
|
|
|
ImFont* font = ImGui::GetFont();
|
|
|
|
|
|
|
|
|
|
|
|
// "Entering:" header
|
|
|
|
|
|
const char* header = "Entering:";
|
|
|
|
|
|
float headerSize = 16.0f;
|
|
|
|
|
|
float nameSize = 26.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImVec2 headerDim = font->CalcTextSizeA(headerSize, FLT_MAX, 0.0f, header);
|
|
|
|
|
|
ImVec2 nameDim = font->CalcTextSizeA(nameSize, FLT_MAX, 0.0f, zoneTextName_.c_str());
|
|
|
|
|
|
|
|
|
|
|
|
float centreY = screenH * 0.30f; // upper third, like WoW
|
|
|
|
|
|
float headerX = (screenW - headerDim.x) * 0.5f;
|
|
|
|
|
|
float nameX = (screenW - nameDim.x) * 0.5f;
|
|
|
|
|
|
float headerY = centreY;
|
|
|
|
|
|
float nameY = centreY + headerDim.y + 4.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImDrawList* draw = ImGui::GetForegroundDrawList();
|
|
|
|
|
|
|
|
|
|
|
|
// "Entering:" in gold
|
|
|
|
|
|
draw->AddText(font, headerSize, ImVec2(headerX + 1, headerY + 1),
|
|
|
|
|
|
IM_COL32(0, 0, 0, (int)(alpha * 160)), header);
|
|
|
|
|
|
draw->AddText(font, headerSize, ImVec2(headerX, headerY),
|
|
|
|
|
|
IM_COL32(255, 215, 0, (int)(alpha * 255)), header);
|
|
|
|
|
|
|
|
|
|
|
|
// Zone name in white
|
|
|
|
|
|
draw->AddText(font, nameSize, ImVec2(nameX + 1, nameY + 1),
|
|
|
|
|
|
IM_COL32(0, 0, 0, (int)(alpha * 160)), zoneTextName_.c_str());
|
|
|
|
|
|
draw->AddText(font, nameSize, ImVec2(nameX, nameY),
|
|
|
|
|
|
IM_COL32(255, 255, 255, (int)(alpha * 255)), zoneTextName_.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
|
rs.x[i] = static_cast<float>(std::rand() % (static_cast<int>(sw) + 200)) - 100.0f;
|
|
|
|
|
|
rs.y[i] = static_cast<float>(std::rand() % static_cast<int>(sh));
|
|
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
|
rs.x[i] = static_cast<float>(std::rand() % (static_cast<int>(sw) + 200)) - 100.0f;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (rs.x[i] > sw + 100.0f) rs.x[i] -= sw + 200.0f;
|
|
|
|
|
|
dl->AddLine(ImVec2(rs.x[i], rs.y[i]),
|
|
|
|
|
|
ImVec2(rs.x[i] + trailDx, rs.y[i] + trailDy),
|
|
|
|
|
|
dropCol, 1.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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) {
|
|
|
|
|
|
ss.x[i] = static_cast<float>(std::rand() % static_cast<int>(sw));
|
|
|
|
|
|
ss.y[i] = static_cast<float>(std::rand() % static_cast<int>(sh));
|
|
|
|
|
|
ss.phase[i] = static_cast<float>(std::rand() % 628) * 0.01f;
|
|
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
|
ss.x[i] = static_cast<float>(std::rand() % static_cast<int>(sw));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ss.x[i] < -5.0f) ss.x[i] += sw + 10.0f;
|
|
|
|
|
|
if (ss.x[i] > sw + 5.0f) ss.x[i] -= sw + 10.0f;
|
|
|
|
|
|
// Two-tone: bright centre dot + transparent outer ring for depth
|
|
|
|
|
|
dl->AddCircleFilled(ImVec2(ss.x[i], ss.y[i]), radius, IM_COL32(220, 235, 255, alphaU8));
|
|
|
|
|
|
dl->AddCircleFilled(ImVec2(ss.x[i], ss.y[i]), radius * 0.45f, IM_COL32(245, 250, 255, std::min(255, alphaU8 + 30)));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-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-14 03:49:42 -07:00
|
|
|
|
if (!chatInputActive && !ImGui::GetIO().WantTextInput &&
|
|
|
|
|
|
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:
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Status: Not queued");
|
|
|
|
|
|
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())
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.3f, 0.9f, 0.3f, 1.0f),
|
|
|
|
|
|
"Status: In queue for %s (%d:%02d)", dName.c_str(), qMin, qSec);
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.3f, 0.9f, 0.3f, 1.0f), "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;
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f),
|
|
|
|
|
|
"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:
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Status: Vote kick in progress");
|
|
|
|
|
|
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())
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.6f, 1.0f, 0.6f, 1.0f), "Status: %s complete", dName.c_str());
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.6f, 1.0f, 0.6f, 1.0f), "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) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "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; };
|
|
|
|
|
|
static const DungeonEntry kDungeons[] = {
|
|
|
|
|
|
{ 861, "Random Dungeon" },
|
|
|
|
|
|
{ 862, "Random Heroic" },
|
|
|
|
|
|
// Vanilla classics
|
|
|
|
|
|
{ 36, "Deadmines" },
|
|
|
|
|
|
{ 43, "Ragefire Chasm" },
|
|
|
|
|
|
{ 47, "Razorfen Kraul" },
|
|
|
|
|
|
{ 48, "Blackfathom Deeps" },
|
|
|
|
|
|
{ 52, "Uldaman" },
|
|
|
|
|
|
{ 57, "Dire Maul: East" },
|
|
|
|
|
|
{ 70, "Onyxia's Lair" },
|
|
|
|
|
|
// TBC heroics
|
|
|
|
|
|
{ 264, "The Blood Furnace" },
|
|
|
|
|
|
{ 269, "The Shattered Halls" },
|
|
|
|
|
|
// WotLK normals/heroics
|
|
|
|
|
|
{ 576, "The Nexus" },
|
|
|
|
|
|
{ 578, "The Oculus" },
|
|
|
|
|
|
{ 595, "The Culling of Stratholme" },
|
|
|
|
|
|
{ 599, "Halls of Stone" },
|
|
|
|
|
|
{ 600, "Drak'Tharon Keep" },
|
|
|
|
|
|
{ 601, "Azjol-Nerub" },
|
|
|
|
|
|
{ 604, "Gundrak" },
|
|
|
|
|
|
{ 608, "Violet Hold" },
|
|
|
|
|
|
{ 619, "Ahn'kahet: Old Kingdom" },
|
|
|
|
|
|
{ 623, "Halls of Lightning" },
|
|
|
|
|
|
{ 632, "The Forge of Souls" },
|
|
|
|
|
|
{ 650, "Trial of the Champion" },
|
|
|
|
|
|
{ 658, "Pit of Saron" },
|
|
|
|
|
|
{ 668, "Halls of Reflection" },
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Find current index
|
|
|
|
|
|
int curIdx = 0;
|
|
|
|
|
|
for (int i = 0; i < (int)(sizeof(kDungeons)/sizeof(kDungeons[0])); ++i) {
|
|
|
|
|
|
if (kDungeons[i].id == lfgSelectedDungeon_) { curIdx = i; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextItemWidth(-1);
|
|
|
|
|
|
if (ImGui::BeginCombo("##dungeon", kDungeons[curIdx].name)) {
|
|
|
|
|
|
for (int i = 0; i < (int)(sizeof(kDungeons)/sizeof(kDungeons[0])); ++i) {
|
|
|
|
|
|
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();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Select at least one role.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 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()) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "No active instance lockouts.");
|
|
|
|
|
|
} 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 {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Expired");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Locked");
|
|
|
|
|
|
} 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, "" },
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "%s", def->name);
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.4f, 0.6f, 1.0f, 1.0f), "%s", aBuf);
|
|
|
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f), "%s", hBuf);
|
|
|
|
|
|
}
|
|
|
|
|
|
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")) {
|
|
|
|
|
|
selectedChatType = 4;
|
|
|
|
|
|
strncpy(whisperTargetBuffer, e.name.c_str(), sizeof(whisperTargetBuffer) - 1);
|
|
|
|
|
|
whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0';
|
|
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
|
ImGui::TextUnformatted(zoneName.empty() ? "Unknown" : zoneName.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
color = e.isPlayerSource ? ImVec4(1.0f, 0.9f, 0.3f, 1.0f) : ImVec4(1.0f, 0.4f, 0.4f, 1.0f);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case T::CRIT_DAMAGE:
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s crits %s for %d!", src, tgt, e.amount);
|
|
|
|
|
|
color = e.isPlayerSource ? ImVec4(1.0f, 1.0f, 0.0f, 1.0f) : ImVec4(1.0f, 0.2f, 0.2f, 1.0f);
|
|
|
|
|
|
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);
|
|
|
|
|
|
color = e.isPlayerSource ? ImVec4(1.0f, 0.9f, 0.3f, 1.0f) : ImVec4(1.0f, 0.4f, 0.4f, 1.0f);
|
|
|
|
|
|
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);
|
|
|
|
|
|
color = ImVec4(0.4f, 1.0f, 0.4f, 1.0f);
|
|
|
|
|
|
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);
|
|
|
|
|
|
color = ImVec4(0.3f, 1.0f, 0.3f, 1.0f);
|
|
|
|
|
|
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);
|
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.65f, 0.65f, 1.0f);
|
|
|
|
|
|
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);
|
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.65f, 0.65f, 1.0f);
|
|
|
|
|
|
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);
|
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.65f, 0.65f, 1.0f);
|
|
|
|
|
|
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);
|
|
|
|
|
|
color = ImVec4(0.65f, 0.65f, 0.65f, 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
|
|
|
|
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);
|
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.8f, 0.8f, 0.8f, 1.0f);
|
|
|
|
|
|
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);
|
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.4f, 0.6f, 1.0f, 1.0f);
|
|
|
|
|
|
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);
|
|
|
|
|
|
color = ImVec4(1.0f, 0.2f, 0.2f, 1.0f);
|
|
|
|
|
|
break;
|
2026-03-17 14:38:57 -07:00
|
|
|
|
case T::HONOR_GAIN:
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "You gain %d honor", e.amount);
|
|
|
|
|
|
color = ImVec4(1.0f, 0.85f, 0.0f, 1.0f);
|
|
|
|
|
|
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:
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "Combat event (type %d, amount %d)", (int)e.type, e.amount);
|
|
|
|
|
|
color = ImVec4(0.7f, 0.7f, 0.7f, 1.0f);
|
|
|
|
|
|
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];
|
|
|
|
|
|
snprintf(earnedLabel, sizeof(earnedLabel), "Earned (%u)###earned", (unsigned)earned.size());
|
|
|
|
|
|
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));
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "\xE2\x98\x85");
|
|
|
|
|
|
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) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f),
|
|
|
|
|
|
"%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;
|
|
|
|
|
|
static const char* kMonths[12] = {
|
|
|
|
|
|
"Jan","Feb","Mar","Apr","May","Jun",
|
|
|
|
|
|
"Jul","Aug","Sep","Oct","Nov","Dec"
|
|
|
|
|
|
};
|
|
|
|
|
|
const char* mname = (month >= 1 && month <= 12) ? kMonths[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];
|
|
|
|
|
|
snprintf(critLabel, sizeof(critLabel), "Criteria (%u)###crit", (unsigned)criteria.size());
|
|
|
|
|
|
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) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.6f, 1.0f, 0.6f, 1.0f),
|
|
|
|
|
|
"%llu/%llu",
|
|
|
|
|
|
static_cast<unsigned long long>(cval),
|
|
|
|
|
|
static_cast<unsigned long long>(ceIt->second.quantity));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.6f, 1.0f, 0.6f, 1.0f),
|
|
|
|
|
|
"%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()) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "GM support is currently unavailable.");
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Show existing open ticket if any
|
|
|
|
|
|
if (gameHandler.hasActiveGmTicket()) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "You have an open GM ticket.");
|
|
|
|
|
|
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
|
|
|
|
|
|
ImVec4 col = ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
|
|
|
|
|
if (rank == 1) col = ImVec4(1.0f, 0.82f, 0.0f, 1.0f); // gold
|
|
|
|
|
|
if (isPlayer && rank == 1) col = ImVec4(1.0f, 0.3f, 0.3f, 1.0f); // red — you have aggro
|
|
|
|
|
|
|
|
|
|
|
|
// Threat bar
|
|
|
|
|
|
float pct = (maxThreat > 0) ? (float)entry.threat / (float)maxThreat : 0.0f;
|
|
|
|
|
|
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);
|
|
|
|
|
|
ImVec4 teamCol = (t == 0) ? ImVec4(1.0f, 0.35f, 0.35f, 1.0f) // team 0: red
|
|
|
|
|
|
: ImVec4(0.4f, 0.6f, 1.0f, 1.0f); // team 1: blue
|
|
|
|
|
|
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();
|
|
|
|
|
|
winnerColor = (data->winner == 0) ? ImVec4(1.0f, 0.35f, 0.35f, 1.0f)
|
|
|
|
|
|
: ImVec4(0.4f, 0.6f, 1.0f, 1.0f);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
winnerStr = (data->winner == 1) ? "Alliance" : "Horde";
|
|
|
|
|
|
winnerColor = (data->winner == 1) ? ImVec4(0.4f, 0.6f, 1.0f, 1.0f)
|
|
|
|
|
|
: ImVec4(1.0f, 0.35f, 0.35f, 1.0f);
|
|
|
|
|
|
}
|
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);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "Victory!");
|
|
|
|
|
|
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)
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.4f, 0.6f, 1.0f, 1.0f), "Alliance");
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f), "Horde");
|
|
|
|
|
|
|
|
|
|
|
|
// Name (highlight player's own row)
|
|
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
|
|
bool isSelf = (ps->guid == playerGuid);
|
|
|
|
|
|
if (isSelf) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f, 0.0f, 1.0f));
|
|
|
|
|
|
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)
|
|
|
|
|
|
static const char* kSlotNames[19] = {
|
|
|
|
|
|
"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());
|
|
|
|
|
|
ImVec4 nameColor = (cid != 0) ? classColorVec4(cid) : ImVec4(1.0f, 0.82f, 0.0f, 1.0f);
|
|
|
|
|
|
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();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "(%u unspent)", result->unspentTalents);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (result->talentGroups > 1) {
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextDisabled(" Dual spec (active %u)", (unsigned)result->activeTalentGroup + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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),
|
|
|
|
|
|
ImVec4(1,1,1,1), qColor);
|
|
|
|
|
|
} 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();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "<-- active");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f, 0.0f, 1.0f));
|
|
|
|
|
|
}
|
|
|
|
|
|
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});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static const struct { uint32_t cat; const char* label; } kCatOrder[] = {
|
|
|
|
|
|
{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
|