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-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-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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
chatTabs_.push_back({"General", 0xFFFFFFFF});
|
|
|
|
|
|
// Combat tab: system + loot messages
|
|
|
|
|
|
chatTabs_.push_back({"Combat", (1u << static_cast<uint8_t>(game::ChatType::SYSTEM)) |
|
|
|
|
|
|
(1u << static_cast<uint8_t>(game::ChatType::LOOT))});
|
|
|
|
|
|
// Whispers tab
|
|
|
|
|
|
chatTabs_.push_back({"Whispers", (1u << static_cast<uint8_t>(game::ChatType::WHISPER)) |
|
|
|
|
|
|
(1u << static_cast<uint8_t>(game::ChatType::WHISPER_INFORM))});
|
|
|
|
|
|
// Trade/LFG tab: channel messages
|
|
|
|
|
|
chatTabs_.push_back({"Trade/LFG", (1u << static_cast<uint8_t>(game::ChatType::CHANNEL))});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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];
|
|
|
|
|
|
if (tab.typeMask == 0xFFFFFFFF) return true; // General tab shows all
|
|
|
|
|
|
|
|
|
|
|
|
uint32_t typeBit = 1u << static_cast<uint8_t>(msg.type);
|
|
|
|
|
|
|
|
|
|
|
|
// For Trade/LFG tab, also filter by channel name
|
|
|
|
|
|
if (tabIndex == 3 && msg.type == game::ChatType::CHANNEL) {
|
|
|
|
|
|
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-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-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-02-17 16:31:00 -08:00
|
|
|
|
// Apply auto-loot setting to GameHandler every frame (cheap bool sync)
|
|
|
|
|
|
gameHandler.setAutoLoot(pendingAutoLoot);
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
// Target frame (only when we have a target)
|
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
|
renderTargetFrame(gameHandler);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
2026-02-10 01:24:37 -08:00
|
|
|
|
renderBagBar(gameHandler);
|
2026-02-05 12:07:58 -08:00
|
|
|
|
renderXpBar(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-09 15:05:38 -07:00
|
|
|
|
renderQuestObjectiveTracker(gameHandler);
|
2026-03-09 17:03:06 -07:00
|
|
|
|
if (showNameplates_) renderNameplates(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);
|
|
|
|
|
|
renderPartyFrames(gameHandler);
|
|
|
|
|
|
renderGroupInvitePopup(gameHandler);
|
2026-03-09 13:58:02 -07:00
|
|
|
|
renderDuelRequestPopup(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-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-02-13 21:39:48 -08:00
|
|
|
|
renderGuildRoster(gameHandler);
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
renderBuffBar(gameHandler);
|
|
|
|
|
|
renderLootWindow(gameHandler);
|
|
|
|
|
|
renderGossipWindow(gameHandler);
|
2026-02-06 11:59:51 -08:00
|
|
|
|
renderQuestDetailsWindow(gameHandler);
|
2026-02-06 21:50:15 -08:00
|
|
|
|
renderQuestRequestItemsWindow(gameHandler);
|
|
|
|
|
|
renderQuestOfferRewardWindow(gameHandler);
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
renderVendorWindow(gameHandler);
|
2026-02-08 14:33:39 -08:00
|
|
|
|
renderTrainerWindow(gameHandler);
|
2026-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-02-09 23:41:38 -08:00
|
|
|
|
// renderQuestMarkers(gameHandler); // Disabled - using 3D billboard markers now
|
2026-02-06 20:10:10 -08:00
|
|
|
|
renderMinimapMarkers(gameHandler);
|
2026-02-06 17:27:20 -08:00
|
|
|
|
renderDeathScreen(gameHandler);
|
2026-02-07 23:12:24 -08:00
|
|
|
|
renderResurrectDialog(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();
|
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)
|
|
|
|
|
|
questLogScreen.render(gameHandler);
|
|
|
|
|
|
|
2026-02-04 11:31:08 -08:00
|
|
|
|
// Spellbook (P key toggle handled inside)
|
|
|
|
|
|
spellbookScreen.render(gameHandler, core::Application::getInstance().getAssetManager());
|
|
|
|
|
|
|
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-02-06 13:47:03 -08:00
|
|
|
|
// Character screen (C key toggle handled inside render())
|
2026-02-06 14:24:38 -08:00
|
|
|
|
inventoryScreen.renderCharacterScreen(gameHandler);
|
2026-02-06 13:47:03 -08:00
|
|
|
|
|
2026-02-06 03:13:42 -08:00
|
|
|
|
if (inventoryScreen.consumeEquipmentDirty() || gameHandler.consumeOnlineEquipmentDirty()) {
|
2026-02-02 12:24:50 -08:00
|
|
|
|
updateCharacterGeosets(gameHandler.getInventory());
|
|
|
|
|
|
updateCharacterTextures(gameHandler.getInventory());
|
|
|
|
|
|
core::Application::getInstance().loadEquippedWeapons();
|
2026-02-06 14:24:38 -08:00
|
|
|
|
inventoryScreen.markPreviewDirty();
|
2026-02-06 15:41:29 -08:00
|
|
|
|
// Update renderer weapon type for animation selection
|
|
|
|
|
|
auto* r = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
if (r) {
|
|
|
|
|
|
const auto& mh = gameHandler.getInventory().getEquipSlot(game::EquipSlot::MAIN_HAND);
|
|
|
|
|
|
r->setEquippedWeaponType(mh.empty() ? 0 : mh.item.inventoryType);
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
|
// Update renderer face-target position and selection circle
|
2026-02-02 12:24:50 -08:00
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
if (renderer) {
|
2026-02-20 03:38:12 -08:00
|
|
|
|
renderer->setInCombat(gameHandler.isInCombat());
|
2026-02-02 12:24:50 -08:00
|
|
|
|
static glm::vec3 targetGLPos;
|
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
|
auto target = gameHandler.getTarget();
|
|
|
|
|
|
if (target) {
|
2026-02-18 03:53:53 -08:00
|
|
|
|
targetGLPos = core::coords::canonicalToRender(
|
2026-02-18 03:57:12 -08:00
|
|
|
|
glm::vec3(target->getX(), target->getY(), target->getZ()));
|
2026-02-20 16:02:34 -08:00
|
|
|
|
float footZ = 0.0f;
|
|
|
|
|
|
if (core::Application::getInstance().getRenderFootZForGuid(target->getGuid(), footZ)) {
|
|
|
|
|
|
targetGLPos.z = footZ;
|
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
|
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-02-14 14:30:09 -08:00
|
|
|
|
// Chat tabs
|
|
|
|
|
|
if (ImGui::BeginTabBar("ChatTabs")) {
|
|
|
|
|
|
for (int i = 0; i < static_cast<int>(chatTabs_.size()); ++i) {
|
|
|
|
|
|
if (ImGui::BeginTabItem(chatTabs_[i].name.c_str())) {
|
|
|
|
|
|
activeChatTab_ = i;
|
|
|
|
|
|
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());
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
ImGui::Text("%.1f DPS", 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-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-02-19 02:27:01 -08:00
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Sell: %ug %us %uc", 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;
|
|
|
|
|
|
ImGui::Text("%.1f DPS", dps);
|
|
|
|
|
|
}
|
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-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-02-14 15:58:54 -08:00
|
|
|
|
// Find next WoW item link: |cXXXXXXXX|Hitem:ENTRY:...|h[Name]|h|r
|
|
|
|
|
|
size_t linkStart = text.find("|c", pos);
|
|
|
|
|
|
// Also handle bare |Hitem: without color prefix
|
|
|
|
|
|
size_t bareLinkStart = text.find("|Hitem:", pos);
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
hStart = text.find("|Hitem:", linkStart + 10);
|
|
|
|
|
|
} else if (nextSpecial == bareLinkStart) {
|
|
|
|
|
|
hStart = bareLinkStart;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (hStart != std::string::npos) {
|
|
|
|
|
|
// Parse item entry: |Hitem:ENTRY:...
|
|
|
|
|
|
size_t entryStart = hStart + 7; // skip "|Hitem:"
|
|
|
|
|
|
size_t entryEnd = text.find(':', entryStart);
|
|
|
|
|
|
uint32_t itemEntry = 0;
|
|
|
|
|
|
if (entryEnd != std::string::npos) {
|
|
|
|
|
|
itemEntry = static_cast<uint32_t>(strtoul(
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
std::string itemName = "Unknown Item";
|
|
|
|
|
|
if (nameTagStart != std::string::npos && nameTagEnd != std::string::npos) {
|
|
|
|
|
|
itemName = text.substr(nameTagStart + 3, nameTagEnd - nameTagStart - 3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Find end of entire link sequence (|r or after ]|h)
|
|
|
|
|
|
size_t linkEnd = (nameTagEnd != std::string::npos) ? nameTagEnd + 3 : hStart + 7;
|
|
|
|
|
|
size_t resetPos = text.find("|r", linkEnd);
|
|
|
|
|
|
if (resetPos != std::string::npos && resetPos <= linkEnd + 2) {
|
|
|
|
|
|
linkEnd = resetPos + 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Ensure item info is cached (trigger query if needed)
|
|
|
|
|
|
if (itemEntry > 0) {
|
|
|
|
|
|
gameHandler.ensureItemInfo(itemEntry);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Render bracketed item name in quality color
|
|
|
|
|
|
std::string display = "[" + itemName + "]";
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, linkColor);
|
|
|
|
|
|
ImGui::TextWrapped("%s", display.c_str());
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
|
|
|
|
if (itemEntry > 0) {
|
|
|
|
|
|
renderItemLinkTooltip(itemEntry);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Shift-click: insert item link into chat input
|
|
|
|
|
|
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-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-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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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 (msg.type == game::ChatType::SYSTEM) {
|
2026-02-19 19:33:02 -08:00
|
|
|
|
renderTextWithLinks(tsPrefix + processedMessage, color);
|
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 if (msg.type == game::ChatType::TEXT_EMOTE) {
|
2026-02-19 19:33:02 -08:00
|
|
|
|
renderTextWithLinks(tsPrefix + processedMessage, color);
|
2026-02-04 11:31:08 -08:00
|
|
|
|
} else if (!msg.senderName.empty()) {
|
2026-03-02 08:31:34 -08:00
|
|
|
|
if (msg.type == game::ChatType::MONSTER_SAY || msg.type == game::ChatType::MONSTER_PARTY) {
|
2026-02-19 19:33:02 -08:00
|
|
|
|
std::string fullMsg = tsPrefix + msg.senderName + " says: " + processedMessage;
|
|
|
|
|
|
renderTextWithLinks(fullMsg, color);
|
2026-03-02 08:31:34 -08:00
|
|
|
|
} else if (msg.type == game::ChatType::MONSTER_YELL) {
|
|
|
|
|
|
std::string fullMsg = tsPrefix + msg.senderName + " yells: " + processedMessage;
|
|
|
|
|
|
renderTextWithLinks(fullMsg, color);
|
|
|
|
|
|
} else if (msg.type == game::ChatType::MONSTER_WHISPER || msg.type == game::ChatType::RAID_BOSS_WHISPER) {
|
|
|
|
|
|
std::string fullMsg = tsPrefix + msg.senderName + " whispers: " + processedMessage;
|
|
|
|
|
|
renderTextWithLinks(fullMsg, color);
|
|
|
|
|
|
} else if (msg.type == game::ChatType::MONSTER_EMOTE || msg.type == game::ChatType::RAID_BOSS_EMOTE) {
|
|
|
|
|
|
std::string fullMsg = tsPrefix + msg.senderName + " " + processedMessage;
|
|
|
|
|
|
renderTextWithLinks(fullMsg, color);
|
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-02-19 19:33:02 -08:00
|
|
|
|
std::string fullMsg = tsPrefix + chDisplay + " [" + msg.senderName + "]: " + processedMessage;
|
|
|
|
|
|
renderTextWithLinks(fullMsg, color);
|
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-02-19 19:33:02 -08:00
|
|
|
|
std::string fullMsg = tsPrefix + "[" + std::string(getChatTypeName(msg.type)) + "] " + msg.senderName + ": " + processedMessage;
|
|
|
|
|
|
renderTextWithLinks(fullMsg, color);
|
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-02-19 19:33:02 -08:00
|
|
|
|
std::string fullMsg = tsPrefix + "[" + std::string(getChatTypeName(msg.type)) + "] " + processedMessage;
|
|
|
|
|
|
renderTextWithLinks(fullMsg, color);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Auto-scroll to bottom
|
|
|
|
|
|
if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) {
|
|
|
|
|
|
ImGui::SetScrollHereY(1.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
|
2026-02-14 18:27:59 -08:00
|
|
|
|
// Reset font scale after chat history
|
|
|
|
|
|
ImGui::SetWindowFontScale(1.0f);
|
|
|
|
|
|
|
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);
|
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
|
|
|
|
const char* chatTypes[] = { "SAY", "YELL", "PARTY", "GUILD", "WHISPER", "RAID", "OFFICER", "BATTLEGROUND", "RAID WARNING", "INSTANCE" };
|
|
|
|
|
|
ImGui::Combo("##ChatType", &selectedChatType, chatTypes, 10);
|
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
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
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;
|
|
|
|
|
|
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;
|
|
|
|
|
|
if (detected >= 0 && selectedChatType != detected) {
|
|
|
|
|
|
selectedChatType = detected;
|
|
|
|
|
|
// Strip the prefix, keep only the message part
|
|
|
|
|
|
std::string remaining = buf.substr(sp + 1);
|
|
|
|
|
|
// For whisper, first word after /w is the target
|
|
|
|
|
|
if (detected == 4) {
|
|
|
|
|
|
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
|
|
|
|
|
|
case 9: inputColor = ImVec4(0.4f, 0.6f, 1.0f, 1.0f); break; // INSTANCE - blue
|
|
|
|
|
|
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);
|
|
|
|
|
|
if (self && self->chatInputMoveCursorToEnd) {
|
|
|
|
|
|
int len = static_cast<int>(std::strlen(data->Buf));
|
|
|
|
|
|
data->CursorPos = len;
|
|
|
|
|
|
data->SelectionStart = len;
|
|
|
|
|
|
data->SelectionEnd = len;
|
|
|
|
|
|
self->chatInputMoveCursorToEnd = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackAlways;
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
|
|
// Tab targeting (when keyboard not captured by UI)
|
|
|
|
|
|
if (!io.WantCaptureKeyboard) {
|
|
|
|
|
|
if (input.isKeyJustPressed(SDL_SCANCODE_TAB)) {
|
|
|
|
|
|
const auto& movement = gameHandler.getMovementInfo();
|
|
|
|
|
|
gameHandler.tabTarget(movement.x, movement.y, movement.z);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (input.isKeyJustPressed(SDL_SCANCODE_ESCAPE)) {
|
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-09 17:03:06 -07:00
|
|
|
|
// V — toggle nameplates (WoW default keybinding)
|
|
|
|
|
|
if (input.isKeyJustPressed(SDL_SCANCODE_V)) {
|
|
|
|
|
|
showNameplates_ = !showNameplates_;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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 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
|
|
|
|
|
|
};
|
|
|
|
|
|
for (int i = 0; i < 12; ++i) {
|
|
|
|
|
|
if (input.isKeyJustPressed(actionBarKeys[i])) {
|
|
|
|
|
|
const auto& bar = gameHandler.getActionBar();
|
|
|
|
|
|
if (bar[i].type == game::ActionBarSlot::SPELL && bar[i].isReady()) {
|
|
|
|
|
|
uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
|
|
|
|
|
|
gameHandler.castSpell(bar[i].id, target);
|
2026-02-06 19:17:35 -08:00
|
|
|
|
} else if (bar[i].type == game::ActionBarSlot::ITEM && bar[i].id != 0) {
|
|
|
|
|
|
gameHandler.useItemById(bar[i].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-03-06 20:00:27 -08:00
|
|
|
|
// Slash key: focus chat input — always works unless already typing in chat
|
|
|
|
|
|
if (!chatInputActive && input.isKeyJustPressed(SDL_SCANCODE_SLASH)) {
|
2026-02-04 11:31:08 -08:00
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
chatInputBuffer[0] = '/';
|
|
|
|
|
|
chatInputBuffer[1] = '\0';
|
2026-02-06 18:34:45 -08:00
|
|
|
|
chatInputMoveCursorToEnd = true;
|
2026-02-04 11:31:08 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-06 20:00:27 -08:00
|
|
|
|
// Enter key: focus chat input (empty) — always works unless already typing
|
|
|
|
|
|
if (!chatInputActive && input.isKeyJustPressed(SDL_SCANCODE_RETURN)) {
|
2026-02-04 11:31:08 -08:00
|
|
|
|
refocusChatInput = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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) {
|
|
|
|
|
|
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;
|
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 &&
|
|
|
|
|
|
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-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-02-20 19:51:04 -08:00
|
|
|
|
// Do not hard-filter by GO type here. Some realms/content
|
|
|
|
|
|
// classify usable objects (including some chests) with types
|
|
|
|
|
|
// that look decorative in cache data.
|
2026-02-16 18:46:44 -08: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
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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-02-20 20:37:55 -08:00
|
|
|
|
// Prefer hostile monsters over nearby gameobjects/others when right-click picking.
|
|
|
|
|
|
if (closestHostileUnitGuid != 0) {
|
|
|
|
|
|
closestGuid = closestHostileUnitGuid;
|
|
|
|
|
|
closestType = game::ObjectType::UNIT;
|
|
|
|
|
|
}
|
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-02-06 17:27:20 -08:00
|
|
|
|
// Name in green (friendly player color) — clickable for self-target
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 1.0f, 0.3f, 1.0f));
|
|
|
|
|
|
if (ImGui::Selectable(playerName.c_str(), false, 0, ImVec2(0, 0))) {
|
|
|
|
|
|
gameHandler.setTarget(gameHandler.getPlayerGuid());
|
|
|
|
|
|
}
|
|
|
|
|
|
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-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
|
|
|
|
// 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-02-02 12:24:50 -08:00
|
|
|
|
// Health bar
|
|
|
|
|
|
float pct = static_cast<float>(playerHp) / static_cast<float>(playerMaxHp);
|
2026-02-06 18:34:45 -08:00
|
|
|
|
ImVec4 hpColor = isDead ? ImVec4(0.5f, 0.5f, 0.5f, 1.0f) : ImVec4(0.2f, 0.8f, 0.2f, 1.0f);
|
|
|
|
|
|
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-02-19 17:08:53 -08:00
|
|
|
|
// Rage (1) and Energy (3) always cap at 100 — show bar even if server
|
|
|
|
|
|
// hasn't sent UNIT_FIELD_MAXPOWER1 yet (warriors start combat at 0 rage).
|
|
|
|
|
|
if (maxPower == 0 && (powerType == 1 || powerType == 3)) 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) {
|
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 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)
|
|
|
|
|
|
case 3: powerColor = ImVec4(0.9f, 0.9f, 0.2f, 1.0f); break; // Energy (yellow)
|
|
|
|
|
|
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-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-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();
|
|
|
|
|
|
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 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)) {
|
|
|
|
|
|
// Entity name and type
|
|
|
|
|
|
std::string name = getEntityName(target);
|
|
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
|
ImVec4 nameColor = hostileColor;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
ImGui::TextColored(nameColor, "%s", name.c_str());
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::TextColored(levelColor, "Lv %u", unit->getLevel());
|
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)
|
|
|
|
|
|
case 3: targetPowerColor = ImVec4(0.9f, 0.9f, 0.2f, 1.0f); break; // Energy (yellow)
|
|
|
|
|
|
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");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
|
|
|
|
|
|
|
int shown = 0;
|
|
|
|
|
|
for (size_t i = 0; i < targetAuras.size() && shown < 16; ++i) {
|
|
|
|
|
|
const auto& aura = targetAuras[i];
|
|
|
|
|
|
if (aura.isEmpty()) continue;
|
|
|
|
|
|
|
|
|
|
|
|
if (shown > 0 && shown % ICONS_PER_ROW != 0) ImGui::SameLine();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(10000 + i));
|
|
|
|
|
|
|
|
|
|
|
|
bool isBuff = (aura.flags & 0x80) == 0;
|
|
|
|
|
|
ImVec4 auraBorderColor = isBuff ? ImVec4(0.2f, 0.8f, 0.2f, 0.9f) : ImVec4(0.8f, 0.2f, 0.2f, 0.9f);
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(cx + 1, cy + 1),
|
|
|
|
|
|
IM_COL32(0, 0, 0, 200), timeStr);
|
|
|
|
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(cx, cy),
|
|
|
|
|
|
IM_COL32(255, 255, 255, 255), timeStr);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-23 06:37:15 -08:00
|
|
|
|
// Tooltip
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
std::string name = spellbookScreen.lookupSpellName(aura.spellId, assetMgr);
|
|
|
|
|
|
if (name.empty()) name = "Spell #" + std::to_string(aura.spellId);
|
2026-03-09 16:37:55 -07:00
|
|
|
|
if (tRemainMs > 0) {
|
|
|
|
|
|
int seconds = tRemainMs / 1000;
|
2026-02-23 06:37:15 -08:00
|
|
|
|
if (seconds < 60) {
|
|
|
|
|
|
ImGui::SetTooltip("%s (%ds)", name.c_str(), seconds);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::SetTooltip("%s (%dm %ds)", name.c_str(), seconds / 60, seconds % 60);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::SetTooltip("%s", name.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), "%s", totName.c_str());
|
|
|
|
|
|
|
|
|
|
|
|
if (totEntity->getType() == game::ObjectType::UNIT ||
|
|
|
|
|
|
totEntity->getType() == game::ObjectType::PLAYER) {
|
|
|
|
|
|
auto totUnit = std::static_pointer_cast<game::Unit>(totEntity);
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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-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-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();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 12:43:32 -08:00
|
|
|
|
// /time command
|
|
|
|
|
|
if (cmdLower == "time") {
|
|
|
|
|
|
gameHandler.queryServerTime();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /played command
|
|
|
|
|
|
if (cmdLower == "played") {
|
|
|
|
|
|
gameHandler.requestPlayedTime();
|
|
|
|
|
|
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-02-07 12:43:32 -08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// /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-02-07 13:44:36 -08:00
|
|
|
|
// Targeting commands
|
|
|
|
|
|
if (cmdLower == "cleartarget") {
|
|
|
|
|
|
gameHandler.clearTarget();
|
|
|
|
|
|
chatInputBuffer[0] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
} 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
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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-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-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) {
|
|
|
|
|
|
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";
|
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::BATTLEGROUND: return "BATTLEGROUND";
|
|
|
|
|
|
case game::ChatType::BATTLEGROUND_LEADER: return "BG LEADER";
|
2026-02-02 12:24:50 -08:00
|
|
|
|
case game::ChatType::WHISPER: return "WHISPER";
|
|
|
|
|
|
case game::ChatType::WHISPER_INFORM: return "TO";
|
|
|
|
|
|
case game::ChatType::SYSTEM: return "SYSTEM";
|
2026-02-13 18:59:09 -08:00
|
|
|
|
case game::ChatType::MONSTER_SAY: return "SAY";
|
|
|
|
|
|
case game::ChatType::MONSTER_YELL: return "YELL";
|
|
|
|
|
|
case game::ChatType::MONSTER_EMOTE: return "EMOTE";
|
2026-02-02 12:24:50 -08:00
|
|
|
|
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-02-02 12:24:50 -08:00
|
|
|
|
default: return "UNKNOWN";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
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-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
|
|
|
|
|
|
|
|
|
|
glm::vec3 playerPos = renderer->getCharacterPosition();
|
|
|
|
|
|
auto* window = app.getWindow();
|
|
|
|
|
|
int screenW = window ? window->getWidth() : 1280;
|
|
|
|
|
|
int screenH = window ? window->getHeight() : 720;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
wm->render(playerPos, screenW, screenH);
|
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();
|
|
|
|
|
|
// Try expansion layout first
|
|
|
|
|
|
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-02-17 14:55:32 -08:00
|
|
|
|
// If the DBC has WotLK-range field count (≥200 fields), it's the binary
|
|
|
|
|
|
// WotLK Spell.dbc (CSV fallback). Use WotLK layout regardless of expansion,
|
|
|
|
|
|
// since Turtle/Classic CSV files are garbled and fall back to WotLK binary.
|
|
|
|
|
|
if (fieldCount >= 200) {
|
|
|
|
|
|
tryLoadIcons(0, 133); // WotLK IconID field
|
|
|
|
|
|
} else if (spellL) {
|
2026-02-17 05:27:03 -08:00
|
|
|
|
tryLoadIcons((*spellL)["ID"], (*spellL)["IconID"]);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Fallback to WotLK field 133 if expansion layout yielded nothing
|
|
|
|
|
|
if (spellIconIds_.empty() && fieldCount > 133) {
|
|
|
|
|
|
tryLoadIcons(0, 133);
|
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-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) {
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
2026-02-05 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
|
|
|
|
|
|
|
|
|
|
float slotSize = 48.0f;
|
|
|
|
|
|
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));
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##ActionBar", nullptr, flags)) {
|
|
|
|
|
|
const auto& bar = gameHandler.getActionBar();
|
|
|
|
|
|
static const char* keyLabels[] = {"1","2","3","4","5","6","7","8","9","0","-","="};
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < 12; ++i) {
|
|
|
|
|
|
if (i > 0) ImGui::SameLine(0, spacing);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::BeginGroup();
|
|
|
|
|
|
ImGui::PushID(i);
|
|
|
|
|
|
|
|
|
|
|
|
const auto& slot = bar[i];
|
|
|
|
|
|
bool onCooldown = !slot.isReady();
|
|
|
|
|
|
|
2026-02-05 15:07:13 -08:00
|
|
|
|
auto getSpellName = [&](uint32_t spellId) -> std::string {
|
2026-02-17 15:41:55 -08:00
|
|
|
|
std::string name = spellbookScreen.lookupSpellName(spellId, assetMgr);
|
|
|
|
|
|
if (!name.empty()) return name;
|
2026-02-05 15:07:13 -08:00
|
|
|
|
return "Spell #" + std::to_string(spellId);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-06 14:30:54 -08:00
|
|
|
|
// Try to get icon texture for this slot
|
2026-02-22 03:32:08 -08:00
|
|
|
|
VkDescriptorSet iconTex = VK_NULL_HANDLE;
|
2026-02-06 19:17:35 -08:00
|
|
|
|
const game::ItemDef* barItemDef = nullptr;
|
2026-02-17 03:50:36 -08:00
|
|
|
|
uint32_t itemDisplayInfoId = 0;
|
|
|
|
|
|
std::string itemNameFromQuery;
|
2026-02-06 14:30:54 -08:00
|
|
|
|
if (slot.type == game::ActionBarSlot::SPELL && slot.id != 0) {
|
|
|
|
|
|
iconTex = getSpellIcon(slot.id, assetMgr);
|
2026-02-06 19:17:35 -08:00
|
|
|
|
} else if (slot.type == game::ActionBarSlot::ITEM && slot.id != 0) {
|
2026-02-17 03:50:36 -08:00
|
|
|
|
// Search backpack
|
2026-02-06 19:17:35 -08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-17 03:50:36 -08:00
|
|
|
|
// Search equipped slots
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Search extra bags
|
|
|
|
|
|
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-06 19:17:35 -08:00
|
|
|
|
if (barItemDef && barItemDef->displayInfoId != 0) {
|
2026-02-17 03:50:36 -08:00
|
|
|
|
itemDisplayInfoId = barItemDef->displayInfoId;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Fallback: use item info cache (from server query responses)
|
|
|
|
|
|
if (itemDisplayInfoId == 0) {
|
|
|
|
|
|
if (auto* info = gameHandler.getItemInfo(slot.id)) {
|
|
|
|
|
|
itemDisplayInfoId = info->displayInfoId;
|
|
|
|
|
|
if (itemNameFromQuery.empty() && !info->name.empty())
|
|
|
|
|
|
itemNameFromQuery = info->name;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (itemDisplayInfoId != 0) {
|
|
|
|
|
|
iconTex = inventoryScreen.getItemIcon(itemDisplayInfoId);
|
2026-02-06 19:17:35 -08:00
|
|
|
|
}
|
2026-02-06 14:30:54 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool clicked = false;
|
|
|
|
|
|
if (iconTex) {
|
|
|
|
|
|
// Render icon-based button
|
|
|
|
|
|
ImVec4 tintColor(1, 1, 1, 1);
|
|
|
|
|
|
ImVec4 bgColor(0.1f, 0.1f, 0.1f, 0.9f);
|
|
|
|
|
|
if (onCooldown) {
|
|
|
|
|
|
tintColor = ImVec4(0.4f, 0.4f, 0.4f, 0.8f);
|
|
|
|
|
|
bgColor = ImVec4(0.1f, 0.1f, 0.1f, 0.8f);
|
2026-02-05 15:07:13 -08:00
|
|
|
|
}
|
2026-02-06 14:30:54 -08:00
|
|
|
|
clicked = ImGui::ImageButton("##icon",
|
|
|
|
|
|
(ImTextureID)(uintptr_t)iconTex,
|
2026-02-15 03:17:51 -08:00
|
|
|
|
ImVec2(slotSize, slotSize),
|
2026-02-06 14:30:54 -08:00
|
|
|
|
ImVec2(0, 0), ImVec2(1, 1),
|
|
|
|
|
|
bgColor, tintColor);
|
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-02-06 14:30:54 -08:00
|
|
|
|
// Fallback to text button
|
|
|
|
|
|
if (onCooldown) {
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.2f, 0.2f, 0.8f));
|
|
|
|
|
|
} 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));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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());
|
2026-02-06 19:17:35 -08:00
|
|
|
|
} 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());
|
2026-02-06 14:30:54 -08:00
|
|
|
|
} else if (slot.type == game::ActionBarSlot::ITEM) {
|
|
|
|
|
|
snprintf(label, sizeof(label), "Item");
|
|
|
|
|
|
} else if (slot.type == game::ActionBarSlot::MACRO) {
|
|
|
|
|
|
snprintf(label, sizeof(label), "Macro");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
snprintf(label, sizeof(label), "--");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
clicked = ImGui::Button(label, ImVec2(slotSize, slotSize));
|
|
|
|
|
|
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-06 20:27:01 -08:00
|
|
|
|
bool rightClicked = ImGui::IsItemClicked(ImGuiMouseButton_Right);
|
2026-02-06 21:25:35 -08:00
|
|
|
|
bool hoveredOnRelease = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) &&
|
2026-02-06 20:36:29 -08:00
|
|
|
|
ImGui::IsMouseReleased(ImGuiMouseButton_Left);
|
2026-02-06 20:27:01 -08:00
|
|
|
|
|
2026-02-06 20:36:29 -08:00
|
|
|
|
// Drop dragged spell from spellbook onto this slot
|
|
|
|
|
|
// (mouse release over slot — button click won't fire since press was in spellbook)
|
|
|
|
|
|
if (hoveredOnRelease && spellbookScreen.isDraggingSpell()) {
|
2026-02-06 20:27:01 -08:00
|
|
|
|
gameHandler.setActionBarSlot(i, game::ActionBarSlot::SPELL,
|
|
|
|
|
|
spellbookScreen.getDragSpellId());
|
|
|
|
|
|
spellbookScreen.consumeDragSpell();
|
2026-02-26 02:33:10 -08:00
|
|
|
|
} else if (hoveredOnRelease && inventoryScreen.isHoldingItem()) {
|
2026-02-06 20:36:29 -08:00
|
|
|
|
// Drop held item from inventory onto action bar
|
|
|
|
|
|
const auto& held = inventoryScreen.getHeldItem();
|
|
|
|
|
|
gameHandler.setActionBarSlot(i, game::ActionBarSlot::ITEM, held.itemId);
|
|
|
|
|
|
inventoryScreen.returnHeldItem(gameHandler.getInventory());
|
2026-02-06 19:24:44 -08:00
|
|
|
|
} else if (clicked && actionBarDragSlot_ >= 0) {
|
|
|
|
|
|
// Dropping a dragged action bar slot onto another slot - swap or place
|
|
|
|
|
|
if (i != actionBarDragSlot_) {
|
|
|
|
|
|
const auto& dragSrc = bar[actionBarDragSlot_];
|
|
|
|
|
|
auto srcType = dragSrc.type;
|
|
|
|
|
|
auto srcId = dragSrc.id;
|
|
|
|
|
|
gameHandler.setActionBarSlot(actionBarDragSlot_, slot.type, slot.id);
|
|
|
|
|
|
gameHandler.setActionBarSlot(i, srcType, srcId);
|
|
|
|
|
|
}
|
|
|
|
|
|
actionBarDragSlot_ = -1;
|
|
|
|
|
|
actionBarDragIcon_ = 0;
|
|
|
|
|
|
} else if (clicked && !slot.isEmpty()) {
|
2026-02-06 20:27:01 -08:00
|
|
|
|
// Left-click on non-empty slot: cast spell or use item
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
if (slot.type == game::ActionBarSlot::SPELL && slot.isReady()) {
|
|
|
|
|
|
uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
|
|
|
|
|
|
gameHandler.castSpell(slot.id, target);
|
2026-02-06 19:17:35 -08:00
|
|
|
|
} 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-02-06 20:27:01 -08:00
|
|
|
|
} else if (rightClicked && !slot.isEmpty()) {
|
|
|
|
|
|
// Right-click on non-empty slot: pick up for dragging
|
|
|
|
|
|
actionBarDragSlot_ = i;
|
|
|
|
|
|
actionBarDragIcon_ = iconTex;
|
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 19:17:35 -08:00
|
|
|
|
// Tooltip
|
2026-02-17 03:50:36 -08:00
|
|
|
|
if (ImGui::IsItemHovered() && !slot.isEmpty() && slot.id != 0) {
|
2026-02-05 15:07:13 -08:00
|
|
|
|
ImGui::BeginTooltip();
|
2026-02-17 03:50:36 -08:00
|
|
|
|
if (slot.type == game::ActionBarSlot::SPELL) {
|
|
|
|
|
|
std::string fullName = getSpellName(slot.id);
|
|
|
|
|
|
ImGui::Text("%s", fullName.c_str());
|
|
|
|
|
|
// Hearthstone: show bind point info
|
|
|
|
|
|
if (slot.id == 8690) {
|
|
|
|
|
|
uint32_t mapId = 0;
|
|
|
|
|
|
glm::vec3 pos;
|
|
|
|
|
|
if (gameHandler.getHomeBind(mapId, pos)) {
|
|
|
|
|
|
const char* mapName = "Unknown";
|
|
|
|
|
|
switch (mapId) {
|
|
|
|
|
|
case 0: mapName = "Eastern Kingdoms"; break;
|
|
|
|
|
|
case 1: mapName = "Kalimdor"; break;
|
|
|
|
|
|
case 530: mapName = "Outland"; break;
|
|
|
|
|
|
case 571: mapName = "Northrend"; break;
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f),
|
|
|
|
|
|
"Home: %s", mapName);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::TextDisabled("Use: Teleport home");
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (slot.type == game::ActionBarSlot::ITEM) {
|
|
|
|
|
|
if (barItemDef && !barItemDef->name.empty()) {
|
|
|
|
|
|
ImGui::Text("%s", barItemDef->name.c_str());
|
|
|
|
|
|
} else if (!itemNameFromQuery.empty()) {
|
|
|
|
|
|
ImGui::Text("%s", itemNameFromQuery.c_str());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::Text("Item #%u", slot.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Show cooldown time remaining
|
|
|
|
|
|
if (onCooldown) {
|
|
|
|
|
|
float cd = slot.cooldownRemaining;
|
|
|
|
|
|
if (cd >= 60.0f) {
|
|
|
|
|
|
int mins = static_cast<int>(cd) / 60;
|
|
|
|
|
|
int secs = static_cast<int>(cd) % 60;
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f),
|
|
|
|
|
|
"Cooldown: %d min %d sec", mins, secs);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f),
|
|
|
|
|
|
"Cooldown: %.1f sec", cd);
|
|
|
|
|
|
}
|
2026-02-06 19:17:35 -08:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTooltip();
|
2026-02-05 15:07:13 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 14:30:54 -08:00
|
|
|
|
// Cooldown overlay
|
|
|
|
|
|
if (onCooldown && iconTex) {
|
|
|
|
|
|
// Draw cooldown text centered over the icon
|
|
|
|
|
|
ImVec2 btnMin = ImGui::GetItemRectMin();
|
|
|
|
|
|
ImVec2 btnMax = ImGui::GetItemRectMax();
|
|
|
|
|
|
char cdText[16];
|
|
|
|
|
|
snprintf(cdText, sizeof(cdText), "%.0f", slot.cooldownRemaining);
|
|
|
|
|
|
ImVec2 textSize = ImGui::CalcTextSize(cdText);
|
|
|
|
|
|
float cx = btnMin.x + (btnMax.x - btnMin.x - textSize.x) * 0.5f;
|
|
|
|
|
|
float cy = btnMin.y + (btnMax.y - btnMin.y - textSize.y) * 0.5f;
|
|
|
|
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(cx, cy),
|
|
|
|
|
|
IM_COL32(255, 255, 0, 255), cdText);
|
|
|
|
|
|
} else if (onCooldown) {
|
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 cdText[16];
|
|
|
|
|
|
snprintf(cdText, sizeof(cdText), "%.0f", slot.cooldownRemaining);
|
|
|
|
|
|
ImGui::SetCursorPosY(ImGui::GetCursorPosY() - slotSize / 2 - 8);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1, 1, 0, 1), "%s", cdText);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Key label below
|
|
|
|
|
|
ImGui::TextDisabled("%s", keyLabels[i]);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
ImGui::EndGroup();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 01:24:37 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Bag Bar
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderBagBar(game::GameHandler& gameHandler) {
|
|
|
|
|
|
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;
|
|
|
|
|
|
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-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-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) {
|
|
|
|
|
|
uint32_t nextLevelXp = gameHandler.getPlayerNextLevelXp();
|
|
|
|
|
|
if (nextLevelXp == 0) return; // No XP data yet (level 80 or not initialized)
|
|
|
|
|
|
|
|
|
|
|
|
uint32_t currentXp = gameHandler.getPlayerXp();
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
// Position just above the action bar
|
|
|
|
|
|
float slotSize = 48.0f;
|
|
|
|
|
|
float spacing = 4.0f;
|
|
|
|
|
|
float padding = 8.0f;
|
|
|
|
|
|
float barW = 12 * slotSize + 11 * spacing + padding * 2;
|
|
|
|
|
|
float barH = slotSize + 24.0f;
|
|
|
|
|
|
float actionBarY = screenH - barH;
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
float xpBarY = actionBarY - xpBarH - 2.0f;
|
|
|
|
|
|
|
|
|
|
|
|
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)) {
|
|
|
|
|
|
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)
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
|
|
ImU32 bg = IM_COL32(15, 15, 20, 220);
|
|
|
|
|
|
ImU32 fg = IM_COL32(148, 51, 238, 255);
|
|
|
|
|
|
ImU32 seg = IM_COL32(35, 35, 45, 255);
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
char overlay[96];
|
2026-02-06 18:34:45 -08:00
|
|
|
|
snprintf(overlay, sizeof(overlay), "%u / %u XP", currentXp, nextLevelXp);
|
|
|
|
|
|
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-02-05 12:07:58 -08:00
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
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 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)) {
|
|
|
|
|
|
float progress = gameHandler.getCastProgress();
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.8f, 0.6f, 0.2f, 1.0f));
|
|
|
|
|
|
|
|
|
|
|
|
char overlay[64];
|
2026-02-17 03:50:36 -08:00
|
|
|
|
uint32_t currentSpellId = gameHandler.getCurrentCastSpellId();
|
2026-02-19 03:17:10 -08:00
|
|
|
|
if (gameHandler.getCurrentCastSpellId() == 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);
|
|
|
|
|
|
if (!spellName.empty())
|
|
|
|
|
|
snprintf(overlay, sizeof(overlay), "%s (%.1fs)", spellName.c_str(), gameHandler.getCastTimeRemaining());
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(overlay, sizeof(overlay), "Casting... (%.1fs)", gameHandler.getCastTimeRemaining());
|
|
|
|
|
|
}
|
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::ProgressBar(progress, ImVec2(-1, 20), overlay);
|
|
|
|
|
|
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) {
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
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-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;
|
|
|
|
|
|
|
|
|
|
|
|
float x = screenW - TRACKER_W - RIGHT_MARGIN;
|
|
|
|
|
|
float y = 200.0f; // below minimap area
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(x, y), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(TRACKER_W, 0), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs |
|
|
|
|
|
|
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoBringToFrontOnFocus;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.55f));
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.0f, 6.0f));
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.0f, 2.0f));
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##QuestTracker", nullptr, flags)) {
|
|
|
|
|
|
int shown = 0;
|
|
|
|
|
|
for (const auto& q : questLog) {
|
|
|
|
|
|
if (q.questId == 0) continue;
|
|
|
|
|
|
if (shown >= MAX_QUESTS) break;
|
|
|
|
|
|
|
|
|
|
|
|
// Quest title in yellow (gold) if complete, white if in progress
|
|
|
|
|
|
ImVec4 titleCol = q.complete ? ImVec4(1.0f, 0.84f, 0.0f, 1.0f)
|
|
|
|
|
|
: ImVec4(1.0f, 1.0f, 0.85f, 1.0f);
|
|
|
|
|
|
ImGui::TextColored(titleCol, "%s", q.title.c_str());
|
|
|
|
|
|
|
|
|
|
|
|
// Objectives line (condensed)
|
|
|
|
|
|
if (q.complete) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.5f, 1.0f, 0.5f, 1.0f), " (Complete)");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Kill counts
|
|
|
|
|
|
for (const auto& [entry, progress] : q.killCounts) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f),
|
|
|
|
|
|
" %u/%u", progress.first, progress.second);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Item counts
|
|
|
|
|
|
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;
|
|
|
|
|
|
const auto* info = gameHandler.getItemInfo(itemId);
|
|
|
|
|
|
const char* itemName = (info && !info->name.empty()) ? info->name.c_str() : nullptr;
|
|
|
|
|
|
if (itemName) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f),
|
|
|
|
|
|
" %s: %u/%u", itemName, count, required);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f),
|
|
|
|
|
|
" Item: %u/%u", count, required);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (q.killCounts.empty() && q.itemCounts.empty() && !q.objectives.empty()) {
|
|
|
|
|
|
// Show the raw objectives text, truncated if needed
|
|
|
|
|
|
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());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (shown < MAX_QUESTS - 1 && shown < static_cast<int>(questLog.size()) - 1) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
}
|
|
|
|
|
|
++shown;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopStyleVar(2);
|
|
|
|
|
|
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
|
|
|
|
// ============================================================
|
|
|
|
|
|
// 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:
|
|
|
|
|
|
snprintf(text, sizeof(text), outgoing ? "Block" : "You Block");
|
|
|
|
|
|
color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha)
|
|
|
|
|
|
: ImVec4(0.4f, 0.9f, 1.0f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::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;
|
|
|
|
|
|
case game::CombatTextEntry::ENVIRONMENTAL:
|
|
|
|
|
|
snprintf(text, sizeof(text), "-%d", entry.amount);
|
|
|
|
|
|
color = ImVec4(0.9f, 0.5f, 0.2f, alpha); // Orange for environmental
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::ENERGIZE:
|
|
|
|
|
|
snprintf(text, sizeof(text), "+%d", entry.amount);
|
|
|
|
|
|
color = ImVec4(0.3f, 0.6f, 1.0f, alpha); // Blue for mana/energy
|
|
|
|
|
|
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;
|
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));
|
|
|
|
|
|
ImGui::TextColored(color, "%s", text);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
// Convert canonical WoW position → render space, raise to head height
|
|
|
|
|
|
glm::vec3 renderPos = core::coords::canonicalToRender(
|
|
|
|
|
|
glm::vec3(unit->getX(), unit->getY(), unit->getZ()));
|
|
|
|
|
|
renderPos.z += 2.3f;
|
|
|
|
|
|
|
|
|
|
|
|
// Cull if too far (render units ≈ WoW yards)
|
|
|
|
|
|
float dist = glm::length(renderPos - camPos);
|
|
|
|
|
|
if (dist > 40.0f) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// Project to clip space
|
|
|
|
|
|
glm::vec4 clipPos = viewProj * glm::vec4(renderPos, 1.0f);
|
|
|
|
|
|
if (clipPos.w <= 0.01f) continue; // Behind camera
|
|
|
|
|
|
|
|
|
|
|
|
glm::vec3 ndc = glm::vec3(clipPos) / clipPos.w;
|
|
|
|
|
|
if (ndc.x < -1.2f || ndc.x > 1.2f || ndc.y < -1.2f || ndc.y > 1.2f) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// NDC → screen pixels (Y axis inverted)
|
|
|
|
|
|
float sx = (ndc.x * 0.5f + 0.5f) * screenW;
|
|
|
|
|
|
float sy = (1.0f - (ndc.y * 0.5f + 0.5f)) * screenH;
|
|
|
|
|
|
|
|
|
|
|
|
// Fade out in the last 5 units of range
|
|
|
|
|
|
float alpha = dist < 35.0f ? 1.0f : 1.0f - (dist - 35.0f) / 5.0f;
|
|
|
|
|
|
auto A = [&](int v) { return static_cast<int>(v * alpha); };
|
|
|
|
|
|
|
|
|
|
|
|
// Bar colour by hostility
|
|
|
|
|
|
ImU32 barColor, bgColor;
|
|
|
|
|
|
if (unit->isHostile()) {
|
|
|
|
|
|
barColor = IM_COL32(220, 60, 60, A(200));
|
|
|
|
|
|
bgColor = IM_COL32(100, 25, 25, A(160));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
barColor = IM_COL32(60, 200, 80, A(200));
|
|
|
|
|
|
bgColor = IM_COL32(25, 100, 35, A(160));
|
|
|
|
|
|
}
|
|
|
|
|
|
ImU32 borderColor = (guid == targetGuid)
|
|
|
|
|
|
? IM_COL32(255, 215, 0, A(255))
|
|
|
|
|
|
: IM_COL32(20, 20, 20, A(180));
|
|
|
|
|
|
|
|
|
|
|
|
// Bar geometry
|
|
|
|
|
|
constexpr float barW = 80.0f;
|
|
|
|
|
|
constexpr float barH = 8.0f;
|
|
|
|
|
|
const float barX = sx - barW * 0.5f;
|
|
|
|
|
|
|
|
|
|
|
|
float healthPct = std::clamp(
|
|
|
|
|
|
static_cast<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);
|
|
|
|
|
|
drawList->AddRectFilled(ImVec2(barX, sy), ImVec2(barX + barW * healthPct, sy + barH), barColor, 2.0f);
|
|
|
|
|
|
drawList->AddRect (ImVec2(barX - 1.0f, sy - 1.0f), ImVec2(barX + barW + 1.0f, sy + barH + 1.0f), borderColor, 2.0f);
|
|
|
|
|
|
|
2026-03-09 17:04:14 -07:00
|
|
|
|
// Name + level label above health bar
|
|
|
|
|
|
uint32_t level = unit->getLevel();
|
|
|
|
|
|
char labelBuf[96];
|
|
|
|
|
|
if (level > 0) {
|
|
|
|
|
|
uint32_t playerLevel = gameHandler.getPlayerLevel();
|
|
|
|
|
|
// Show skull for units more than 10 levels above the player
|
|
|
|
|
|
if (playerLevel > 0 && level > playerLevel + 10)
|
|
|
|
|
|
snprintf(labelBuf, sizeof(labelBuf), "?? %s", unit->getName().c_str());
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(labelBuf, sizeof(labelBuf), "%u %s", level, unit->getName().c_str());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
snprintf(labelBuf, sizeof(labelBuf), "%s", unit->getName().c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
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-09 17:04:14 -07:00
|
|
|
|
drawList->AddText(ImVec2(nameX + 1.0f, nameY + 1.0f), IM_COL32(0, 0, 0, A(160)), labelBuf);
|
|
|
|
|
|
drawList->AddText(ImVec2(nameX, nameY), IM_COL32(255, 255, 255, A(220)), labelBuf);
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
const auto& partyData = gameHandler.getPartyData();
|
|
|
|
|
|
float frameY = 120.0f;
|
|
|
|
|
|
|
|
|
|
|
|
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)) {
|
|
|
|
|
|
for (const auto& member : partyData.members) {
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(member.guid));
|
|
|
|
|
|
|
2026-02-26 10:25:55 -08:00
|
|
|
|
// Name with level and status info
|
|
|
|
|
|
std::string label = member.name;
|
|
|
|
|
|
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)";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
// Clickable name to target
|
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-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-02-26 10:25:55 -08:00
|
|
|
|
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));
|
|
|
|
|
|
ImGui::ProgressBar(pct, ImVec2(-1, 12), "");
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Power bar (mana/rage/energy) from party stats
|
|
|
|
|
|
if (member.hasPartyStats && member.maxPower > 0) {
|
|
|
|
|
|
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)
|
|
|
|
|
|
case 3: powerColor = ImVec4(0.9f, 0.9f, 0.2f, 1.0f); break; // Energy (yellow)
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// 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-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-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)
|
|
|
|
|
|
};
|
|
|
|
|
|
uint8_t q = roll.itemQuality;
|
|
|
|
|
|
ImVec4 col = (q < 6) ? kQualityColors[q] : kQualityColors[1];
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Text("An item is up for rolls:");
|
|
|
|
|
|
ImGui::TextColored(col, "[%s]", roll.itemName.c_str());
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 21:39:48 -08:00
|
|
|
|
void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) {
|
|
|
|
|
|
// O key toggle (WoW default Social/Guild keybind)
|
|
|
|
|
|
if (!ImGui::GetIO().WantCaptureKeyboard && ImGui::IsKeyPressed(ImGuiKey_O)) {
|
|
|
|
|
|
showGuildRoster_ = !showGuildRoster_;
|
|
|
|
|
|
if (showGuildRoster_) {
|
|
|
|
|
|
if (!gameHandler.isInGuild()) {
|
|
|
|
|
|
gameHandler.addLocalChatMessage(game::MessageChatData{
|
|
|
|
|
|
game::ChatType::SYSTEM, game::ChatLanguage::UNIVERSAL, 0, "", 0, "", "You are not in a guild.", "", 0});
|
|
|
|
|
|
showGuildRoster_ = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-14 15:05:18 -08:00
|
|
|
|
// 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-13 21:39:48 -08:00
|
|
|
|
gameHandler.requestGuildRoster();
|
2026-02-25 14:44:44 -08:00
|
|
|
|
gameHandler.requestGuildInfo();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
ImGui::Text("Cost: %ug %us %uc", gold, silver, copper);
|
|
|
|
|
|
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-02-25 14:44:44 -08:00
|
|
|
|
std::string title = gameHandler.isInGuild() ? (gameHandler.getGuildName() + " - Guild") : "Guild";
|
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;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
static const char* classNames[] = {
|
|
|
|
|
|
"Unknown", "Warrior", "Paladin", "Hunter", "Rogue",
|
|
|
|
|
|
"Priest", "Death Knight", "Shaman", "Mage", "Warlock",
|
|
|
|
|
|
"", "Druid"
|
|
|
|
|
|
};
|
2026-02-13 21:39:48 -08:00
|
|
|
|
|
2026-02-25 14:44:44 -08:00
|
|
|
|
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-02-13 21:39:48 -08:00
|
|
|
|
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
|
|
ImGui::TextColored(textColor, "%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();
|
|
|
|
|
|
const char* className = (m.classId < 12) ? classNames[m.classId] : "Unknown";
|
|
|
|
|
|
ImGui::TextColored(textColor, "%s", className);
|
|
|
|
|
|
|
|
|
|
|
|
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")) {
|
|
|
|
|
|
ImGui::Text("%s", selectedGuildMember_.c_str());
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Promote")) {
|
|
|
|
|
|
gameHandler.promoteGuildMember(selectedGuildMember_);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Demote")) {
|
|
|
|
|
|
gameHandler.demoteGuildMember(selectedGuildMember_);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Kick")) {
|
|
|
|
|
|
gameHandler.kickGuildMember(selectedGuildMember_);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Set Public Note...")) {
|
|
|
|
|
|
showGuildNoteEdit_ = true;
|
|
|
|
|
|
editingOfficerNote_ = false;
|
|
|
|
|
|
guildNoteEditBuffer_[0] = '\0';
|
|
|
|
|
|
// Pre-fill with existing note
|
|
|
|
|
|
for (const auto& mem : roster.members) {
|
|
|
|
|
|
if (mem.name == selectedGuildMember_) {
|
|
|
|
|
|
snprintf(guildNoteEditBuffer_, sizeof(guildNoteEditBuffer_), "%s", mem.publicNote.c_str());
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Set Officer Note...")) {
|
|
|
|
|
|
showGuildNoteEdit_ = true;
|
|
|
|
|
|
editingOfficerNote_ = true;
|
|
|
|
|
|
guildNoteEditBuffer_[0] = '\0';
|
|
|
|
|
|
for (const auto& mem : roster.members) {
|
|
|
|
|
|
if (mem.name == selectedGuildMember_) {
|
|
|
|
|
|
snprintf(guildNoteEditBuffer_, sizeof(guildNoteEditBuffer_), "%s", mem.officerNote.c_str());
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Set as Leader")) {
|
|
|
|
|
|
gameHandler.setGuildLeader(selectedGuildMember_);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
2026-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
|
|
|
|
|
|
|
|
|
|
ImGui::EndTabBar();
|
2026-02-13 21:39:48 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
showGuildRoster_ = open;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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-02-10 13:44:22 -08:00
|
|
|
|
// Position below the player frame in top-left
|
2026-02-07 23:47:43 -08:00
|
|
|
|
constexpr float ICON_SIZE = 32.0f;
|
|
|
|
|
|
constexpr int ICONS_PER_ROW = 8;
|
|
|
|
|
|
float barW = ICONS_PER_ROW * (ICON_SIZE + 4.0f) + 8.0f;
|
2026-02-10 13:44:22 -08:00
|
|
|
|
// Dock under player frame in top-left (player frame is at 10, 30 with ~110px height)
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(10.0f, 145.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)) {
|
|
|
|
|
|
int shown = 0;
|
|
|
|
|
|
for (size_t i = 0; i < auras.size() && shown < 16; ++i) {
|
|
|
|
|
|
const auto& aura = auras[i];
|
|
|
|
|
|
if (aura.isEmpty()) continue;
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(i));
|
|
|
|
|
|
|
2026-02-17 15:49:12 -08:00
|
|
|
|
bool isBuff = (aura.flags & 0x80) == 0; // 0x80 = negative/debuff flag
|
2026-02-07 23:47:43 -08:00
|
|
|
|
ImVec4 borderColor = isBuff ? ImVec4(0.2f, 0.8f, 0.2f, 0.9f) : ImVec4(0.8f, 0.2f, 0.2f, 0.9f);
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
// 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),
|
|
|
|
|
|
IM_COL32(255, 255, 255, 255), timeStr);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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-09 16:36:58 -07:00
|
|
|
|
// Tooltip with spell name and countdown
|
2026-02-07 23:47:43 -08:00
|
|
|
|
if (ImGui::IsItemHovered()) {
|
2026-02-17 15:41:55 -08:00
|
|
|
|
std::string name = spellbookScreen.lookupSpellName(aura.spellId, assetMgr);
|
|
|
|
|
|
if (name.empty()) name = "Spell #" + std::to_string(aura.spellId);
|
2026-03-09 16:36:58 -07:00
|
|
|
|
if (remainMs > 0) {
|
|
|
|
|
|
int seconds = remainMs / 1000;
|
2026-02-07 23:47:43 -08:00
|
|
|
|
if (seconds < 60) {
|
|
|
|
|
|
ImGui::SetTooltip("%s (%ds)", name.c_str(), seconds);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::SetTooltip("%s (%dm %ds)", name.c_str(), seconds / 60, seconds % 60);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::SetTooltip("%s", name.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
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
shown++;
|
|
|
|
|
|
}
|
2026-02-26 10:41:29 -08:00
|
|
|
|
// Dismiss Pet button
|
|
|
|
|
|
if (gameHandler.hasPet()) {
|
|
|
|
|
|
if (shown > 0) ImGui::Spacing();
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.2f, 0.2f, 0.9f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.3f, 0.3f, 1.0f));
|
|
|
|
|
|
if (ImGui::Button("Dismiss Pet", ImVec2(-1, 0))) {
|
|
|
|
|
|
gameHandler.dismissPet();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
}
|
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();
|
|
|
|
|
|
|
|
|
|
|
|
// Gold
|
|
|
|
|
|
if (loot.gold > 0) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%ug %us %uc",
|
|
|
|
|
|
loot.getGold(), loot.getSilver(), loot.getCopper());
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
// 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))) {
|
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
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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());
|
|
|
|
|
|
|
|
|
|
|
|
// Draw count if > 1
|
|
|
|
|
|
if (item.count > 1) {
|
|
|
|
|
|
char countStr[32];
|
|
|
|
|
|
snprintf(countStr, sizeof(countStr), "x%u", item.count);
|
|
|
|
|
|
float countY = textY + ImGui::GetTextLineHeight();
|
|
|
|
|
|
drawList->AddText(ImVec2(textX, countY), IM_COL32(200, 200, 200, 220), countStr);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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) {
|
|
|
|
|
|
gameHandler.lootItem(static_cast<uint8_t>(lootSlotClicked));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
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-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-02-06 11:45:35 -08:00
|
|
|
|
char qlabel[256];
|
|
|
|
|
|
snprintf(qlabel, sizeof(qlabel), "[%d] %s", quest.questLevel, quest.title.c_str());
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 0.3f, 1.0f));
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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 gold = quest.rewardMoney / 10000;
|
|
|
|
|
|
uint32_t silver = (quest.rewardMoney % 10000) / 100;
|
|
|
|
|
|
uint32_t copper = quest.rewardMoney % 100;
|
|
|
|
|
|
if (gold > 0) ImGui::Text(" %ug %us %uc", gold, silver, copper);
|
|
|
|
|
|
else if (silver > 0) ImGui::Text(" %us %uc", silver, copper);
|
|
|
|
|
|
else ImGui::Text(" %uc", copper);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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-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;
|
|
|
|
|
|
if (name && *name) {
|
|
|
|
|
|
ImGui::TextColored(enough ? ImVec4(0.6f, 1.0f, 0.6f, 1.0f) : ImVec4(1.0f, 0.6f, 0.6f, 1.0f),
|
|
|
|
|
|
" %s %u/%u", name, have, item.count);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(enough ? ImVec4(0.6f, 1.0f, 0.6f, 1.0f) : ImVec4(1.0f, 0.6f, 0.6f, 1.0f),
|
|
|
|
|
|
" Item %u %u/%u", item.itemId, have, item.count);
|
|
|
|
|
|
}
|
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;
|
|
|
|
|
|
ImGui::Text("Required money: %ug %us %uc", g, s, c);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
bool selected = (selectedChoice == static_cast<int>(i));
|
2026-02-10 01:24:37 -08:00
|
|
|
|
|
|
|
|
|
|
// Get item icon if we have displayInfoId
|
2026-02-22 03:32:08 -08:00
|
|
|
|
VkDescriptorSet iconTex = VK_NULL_HANDLE;
|
2026-02-10 01:24:37 -08:00
|
|
|
|
if (info && info->valid && info->displayInfoId != 0) {
|
|
|
|
|
|
iconTex = inventoryScreen.getItemIcon(info->displayInfoId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Quality color
|
|
|
|
|
|
ImVec4 qualityColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White (poor)
|
|
|
|
|
|
if (info && info->valid) {
|
|
|
|
|
|
switch (info->quality) {
|
|
|
|
|
|
case 1: qualityColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); break; // Common (white)
|
|
|
|
|
|
case 2: qualityColor = ImVec4(0.0f, 1.0f, 0.0f, 1.0f); break; // Uncommon (green)
|
|
|
|
|
|
case 3: qualityColor = ImVec4(0.0f, 0.5f, 1.0f, 1.0f); break; // Rare (blue)
|
|
|
|
|
|
case 4: qualityColor = ImVec4(0.64f, 0.21f, 0.93f, 1.0f); break; // Epic (purple)
|
|
|
|
|
|
case 5: qualityColor = ImVec4(1.0f, 0.5f, 0.0f, 1.0f); break; // Legendary (orange)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-19 03:31:49 -08:00
|
|
|
|
// Render item with icon + visible selectable label
|
2026-02-10 01:24:37 -08:00
|
|
|
|
ImGui::PushID(static_cast<int>(i));
|
2026-02-19 03:31:49 -08: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);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::Selectable(label.c_str(), selected, 0, ImVec2(0, 24))) {
|
2026-02-06 21:50:15 -08:00
|
|
|
|
selectedChoice = static_cast<int>(i);
|
|
|
|
|
|
}
|
2026-02-19 03:31:49 -08:00
|
|
|
|
if (ImGui::IsItemHovered() && iconTex) {
|
|
|
|
|
|
ImGui::SetTooltip("Reward option");
|
|
|
|
|
|
}
|
2026-02-10 01:24:37 -08:00
|
|
|
|
if (iconTex) {
|
|
|
|
|
|
ImGui::SameLine();
|
2026-02-19 03:31:49 -08:00
|
|
|
|
ImGui::Image((void*)(intptr_t)iconTex, ImVec2(18, 18));
|
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);
|
|
|
|
|
|
if (info && info->valid)
|
|
|
|
|
|
ImGui::Text(" %s x%u", info->name.c_str(), item.count);
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::Text(" Item %u x%u", item.itemId, item.count);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
if (g > 0) ImGui::Text(" %ug %us %uc", g, s, c);
|
|
|
|
|
|
else if (s > 0) ImGui::Text(" %us %uc", s, c);
|
|
|
|
|
|
else ImGui::Text(" %uc", c);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
ImGui::Text("Your money: %ug %us %uc", mg, ms, mc);
|
|
|
|
|
|
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");
|
|
|
|
|
|
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");
|
|
|
|
|
|
if (ImGui::BeginTable("BuybackTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
|
|
|
|
|
|
ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch);
|
|
|
|
|
|
ImGui::TableSetupColumn("Price", ImGuiTableColumnFlags_WidthFixed, 110.0f);
|
|
|
|
|
|
ImGui::TableSetupColumn("Buy", ImGuiTableColumnFlags_WidthFixed, 62.0f);
|
|
|
|
|
|
ImGui::TableHeadersRow();
|
|
|
|
|
|
// Show only the most recently sold item (LIFO).
|
|
|
|
|
|
const int i = 0;
|
|
|
|
|
|
const auto& entry = buyback[0];
|
|
|
|
|
|
uint32_t sellPrice = entry.item.sellPrice;
|
|
|
|
|
|
if (sellPrice == 0) {
|
|
|
|
|
|
if (auto* info = gameHandler.getItemInfo(entry.item.itemId); info && info->valid) {
|
|
|
|
|
|
sellPrice = info->sellPrice;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
uint64_t price = static_cast<uint64_t>(sellPrice) *
|
|
|
|
|
|
static_cast<uint64_t>(entry.count > 0 ? entry.count : 1);
|
|
|
|
|
|
uint32_t g = static_cast<uint32_t>(price / 10000);
|
|
|
|
|
|
uint32_t s = static_cast<uint32_t>((price / 100) % 100);
|
|
|
|
|
|
uint32_t c = static_cast<uint32_t>(price % 100);
|
|
|
|
|
|
bool canAfford = money >= price;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
|
|
ImGui::PushID(8000 + i);
|
|
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
|
|
|
|
|
const char* name = entry.item.name.empty() ? "Unknown Item" : entry.item.name.c_str();
|
|
|
|
|
|
if (entry.count > 1) {
|
|
|
|
|
|
ImGui::Text("%s x%u", name, entry.count);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::Text("%s", name);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
|
|
|
|
|
if (!canAfford) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
|
|
|
|
|
ImGui::Text("%ug %us %uc", g, s, c);
|
|
|
|
|
|
if (!canAfford) ImGui::PopStyleColor();
|
|
|
|
|
|
ImGui::TableSetColumnIndex(2);
|
|
|
|
|
|
if (!canAfford) ImGui::BeginDisabled();
|
|
|
|
|
|
if (ImGui::SmallButton("Buy Back##buyback_0")) {
|
|
|
|
|
|
gameHandler.buyBackItem(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!canAfford) ImGui::EndDisabled();
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
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 {
|
|
|
|
|
|
if (ImGui::BeginTable("VendorTable", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) {
|
|
|
|
|
|
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-02-06 11:59:51 -08:00
|
|
|
|
// Quality colors (matching WoW)
|
|
|
|
|
|
static const ImVec4 qualityColors[] = {
|
|
|
|
|
|
ImVec4(0.6f, 0.6f, 0.6f, 1.0f), // 0 Poor (gray)
|
|
|
|
|
|
ImVec4(1.0f, 1.0f, 1.0f, 1.0f), // 1 Common (white)
|
|
|
|
|
|
ImVec4(0.12f, 1.0f, 0.0f, 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-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
|
|
|
|
ImGui::TableNextRow();
|
2026-02-17 18:55:02 -08:00
|
|
|
|
ImGui::PushID(vi);
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
|
|
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
2026-02-06 11:59:51 -08:00
|
|
|
|
auto* info = gameHandler.getItemInfo(item.itemId);
|
|
|
|
|
|
if (info && info->valid) {
|
|
|
|
|
|
uint32_t q = info->quality < 6 ? info->quality : 1;
|
|
|
|
|
|
ImGui::TextColored(qualityColors[q], "%s", info->name.c_str());
|
|
|
|
|
|
// Tooltip with stats on hover
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
ImGui::TextColored(qualityColors[q], "%s", info->name.c_str());
|
2026-02-18 03:46:03 -08:00
|
|
|
|
if (info->damageMax > 0.0f) {
|
|
|
|
|
|
ImGui::Text("%.0f - %.0f Damage", info->damageMin, info->damageMax);
|
|
|
|
|
|
if (info->delayMs > 0) {
|
|
|
|
|
|
float speed = static_cast<float>(info->delayMs) / 1000.0f;
|
|
|
|
|
|
float dps = ((info->damageMin + info->damageMax) * 0.5f) / speed;
|
|
|
|
|
|
ImGui::Text("Speed %.2f", speed);
|
|
|
|
|
|
ImGui::Text("%.1f damage per second", dps);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-06 11:59:51 -08:00
|
|
|
|
if (info->armor > 0) ImGui::Text("Armor: %d", info->armor);
|
|
|
|
|
|
if (info->stamina > 0) ImGui::Text("+%d Stamina", info->stamina);
|
|
|
|
|
|
if (info->strength > 0) ImGui::Text("+%d Strength", info->strength);
|
|
|
|
|
|
if (info->agility > 0) ImGui::Text("+%d Agility", info->agility);
|
|
|
|
|
|
if (info->intellect > 0) ImGui::Text("+%d Intellect", info->intellect);
|
|
|
|
|
|
if (info->spirit > 0) ImGui::Text("+%d Spirit", info->spirit);
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
|
|
|
|
|
} 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
|
|
|
|
|
|
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
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;
|
|
|
|
|
|
if (!canAfford) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
|
|
|
|
|
ImGui::Text("%ug %us %uc", g, s, c);
|
|
|
|
|
|
if (!canAfford) 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
|
|
|
|
|
|
|
|
|
|
ImGui::TableSetColumnIndex(2);
|
|
|
|
|
|
if (item.maxCount < 0) {
|
|
|
|
|
|
ImGui::Text("Inf");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::Text("%d", item.maxCount);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TableSetColumnIndex(3);
|
2026-02-19 05:48:40 -08:00
|
|
|
|
std::string buyBtnId = "Buy##vendor_" + std::to_string(vi);
|
|
|
|
|
|
if (ImGui::SmallButton(buyBtnId.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
|
|
|
|
gameHandler.buyItem(vendor.vendorGuid, item.itemId, item.slot, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 225, 100), ImGuiCond_Appearing);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(500, 450), ImGuiCond_Appearing);
|
|
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
|
if (ImGui::Begin("Trainer", &open)) {
|
|
|
|
|
|
const auto& trainer = gameHandler.getTrainerSpells();
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
ImGui::Text("Your money: %ug %us %uc", mg, ms, mc);
|
2026-02-10 01:24:37 -08:00
|
|
|
|
|
|
|
|
|
|
// Filter checkbox
|
|
|
|
|
|
static bool showUnavailable = false;
|
|
|
|
|
|
ImGui::Checkbox("Show unavailable spells", &showUnavailable);
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Spell name
|
|
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
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()) {
|
|
|
|
|
|
ImGui::Text("%s", name.c_str());
|
|
|
|
|
|
if (!rank.empty()) ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", rank.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Text("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
|
|
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
2026-02-08 14:46:01 -08:00
|
|
|
|
ImGui::TextColored(color, "%u", spell->reqLevel);
|
2026-02-08 14:33:39 -08:00
|
|
|
|
|
|
|
|
|
|
// Cost
|
|
|
|
|
|
ImGui::TableSetColumnIndex(2);
|
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-02-08 14:33:39 -08:00
|
|
|
|
ImVec4 costColor = canAfford ? color : ImVec4(1.0f, 0.3f, 0.3f, 1.0f);
|
|
|
|
|
|
ImGui::TextColored(costColor, "%ug %us %uc", g, s, c);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(color, "Free");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 15:03:43 -08:00
|
|
|
|
// Train button - only enabled if available, affordable, prereqs met
|
2026-02-08 14:33:39 -08:00
|
|
|
|
ImGui::TableSetColumnIndex(3);
|
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-02-08 14:46:01 -08:00
|
|
|
|
if (!canTrain) ImGui::BeginDisabled();
|
2026-02-08 14:33:39 -08:00
|
|
|
|
if (ImGui::SmallButton("Train")) {
|
2026-02-08 14:46:01 -08:00
|
|
|
|
gameHandler.trainSpell(spell->spellId);
|
2026-02-08 14:33:39 -08:00
|
|
|
|
}
|
2026-02-08 14:46:01 -08:00
|
|
|
|
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) {
|
|
|
|
|
|
if (ImGui::BeginTable(tableId, 4,
|
|
|
|
|
|
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) {
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
|
ImVec2 size(260.0f, 220.0f);
|
|
|
|
|
|
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-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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
if (gold > 0) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.9f, 0.8f, 0.3f, 1.0f), "%ug %us %uc", gold, silver, copper);
|
|
|
|
|
|
} else if (silver > 0) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f), "%us %uc", silver, copper);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.72f, 0.45f, 0.2f, 1.0f), "%uc", copper);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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-02-06 17:27:20 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Death Screen
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) {
|
2026-02-07 23:12:24 -08:00
|
|
|
|
if (!gameHandler.showDeathDialog()) return;
|
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
|
|
|
|
|
|
float dlgW = 280.0f;
|
|
|
|
|
|
float dlgH = 100.0f;
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
const char* text = "Return to life?";
|
|
|
|
|
|
float textW = ImGui::CalcTextSize(text).x;
|
|
|
|
|
|
ImGui::SetCursorPosX((dlgW - textW) / 2);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "%s", text);
|
|
|
|
|
|
|
|
|
|
|
|
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-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-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
|
|
|
|
|
|
|
|
|
|
if (ImGui::Checkbox("Fullscreen", &pendingFullscreen)) {
|
|
|
|
|
|
window->setFullscreen(pendingFullscreen);
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::Checkbox("VSync", &pendingVsync)) {
|
|
|
|
|
|
window->setVsync(pendingVsync);
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::Checkbox("Shadows", &pendingShadows)) {
|
|
|
|
|
|
if (renderer) renderer->setShadowsEnabled(pendingShadows);
|
|
|
|
|
|
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);
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-06 19:15:34 -08:00
|
|
|
|
if (ImGui::Checkbox("Water Refraction", &pendingWaterRefraction)) {
|
|
|
|
|
|
if (renderer) renderer->setWaterRefractionEnabled(pendingWaterRefraction);
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
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]);
|
|
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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
|
|
|
|
|
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-02-09 17:12:35 -08:00
|
|
|
|
window->setFullscreen(pendingFullscreen);
|
|
|
|
|
|
window->setVsync(pendingVsync);
|
|
|
|
|
|
window->applyResolution(kResolutions[pendingResIndex][0], kResolutions[pendingResIndex][1]);
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// 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
|
|
|
|
|
|
|
|
|
|
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-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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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-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-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-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();
|
|
|
|
|
|
bearing = std::atan2(-fwd.x, fwd.y);
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
// Match minimap shader transform exactly.
|
|
|
|
|
|
// Render axes: +X=west, +Y=north. Minimap screen axes: +X=right(east), +Y=down(south).
|
|
|
|
|
|
float rx = -dx * cosB + dy * sinB;
|
|
|
|
|
|
float ry = -dx * sinB - dy * cosB;
|
|
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Optional base nearby NPC dots (independent of quest status packets).
|
|
|
|
|
|
if (minimapNpcDots_) {
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
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-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-02-09 17:39:21 -08: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-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-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-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
|
|
|
|
|
|
|
|
|
|
// "New Mail" indicator below the minimap
|
|
|
|
|
|
if (gameHandler.hasNewMail()) {
|
|
|
|
|
|
float indicatorX = centerX - mapRadius;
|
|
|
|
|
|
float indicatorY = centerY + mapRadius + 4.0f;
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(indicatorX, indicatorY), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(mapRadius * 2.0f, 22), ImGuiCond_Always);
|
|
|
|
|
|
ImGuiWindowFlags mailFlags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
|
|
|
|
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
|
|
|
|
|
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoInputs;
|
|
|
|
|
|
if (ImGui::Begin("##NewMailIndicator", nullptr, mailFlags)) {
|
|
|
|
|
|
// Pulsing effect
|
|
|
|
|
|
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-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;
|
|
|
|
|
|
float screenY = (1.0f - (ndc.y * 0.5f + 0.5f)) * screenH; // Flip Y
|
|
|
|
|
|
|
|
|
|
|
|
// 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-02-13 22:51:49 -08:00
|
|
|
|
out << "separate_bags=" << (pendingSeparateBags ? 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-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-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-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-02-09 17:39:21 -08: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
|
|
|
|
|
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-02-13 22:51:49 -08:00
|
|
|
|
} else if (key == "separate_bags") {
|
|
|
|
|
|
pendingSeparateBags = (std::stoi(val) != 0);
|
|
|
|
|
|
inventoryScreen.setSeparateBags(pendingSeparateBags);
|
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-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-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-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-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
|
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
|
ImGui::Text("Your money: %ug %us %uc", mg, ms, mc);
|
|
|
|
|
|
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]");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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]");
|
|
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
|
ImGui::Text("Money: %ug %us %uc", g, s, c);
|
|
|
|
|
|
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());
|
|
|
|
|
|
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);
|
|
|
|
|
|
if (info && info->valid) {
|
|
|
|
|
|
ImGui::BulletText("%s x%u", info->name.c_str(), att.stackCount);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::BulletText("Item %u x%u", att.itemId, att.stackCount);
|
|
|
|
|
|
gameHandler.ensureItemInfo(att.itemId);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::SmallButton("Take")) {
|
|
|
|
|
|
gameHandler.mailTakeItem(mail.messageId, att.slot);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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) {
|
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::BeginTooltip();
|
2026-02-26 13:38:29 -08:00
|
|
|
|
ImGui::TextColored(qc, "%s", item.name.c_str());
|
|
|
|
|
|
if (item.stackCount > 1) ImGui::Text("Count: %u", item.stackCount);
|
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::EndTooltip();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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);
|
|
|
|
|
|
ImGui::Text("Guild Bank Money: ");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.9f, 0.8f, 0.3f, 1.0f), "%ug %us %uc", gold, silver, copper);
|
|
|
|
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
|
for (size_t i = 0; i < data.tabItems.size(); i++) {
|
|
|
|
|
|
if (i % 14 != 0) ImGui::SameLine();
|
|
|
|
|
|
const auto& item = data.tabItems[i];
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(i) + 5000);
|
|
|
|
|
|
|
|
|
|
|
|
if (item.itemEntry == 0) {
|
|
|
|
|
|
ImGui::Button("##gb", ImVec2(34, 34));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
auto* info = gameHandler.getItemInfo(item.itemEntry);
|
|
|
|
|
|
game::ItemQuality quality = game::ItemQuality::COMMON;
|
|
|
|
|
|
std::string name = "Item " + std::to_string(item.itemEntry);
|
|
|
|
|
|
if (info) {
|
|
|
|
|
|
quality = static_cast<game::ItemQuality>(info->quality);
|
|
|
|
|
|
name = info->name;
|
|
|
|
|
|
}
|
|
|
|
|
|
ImVec4 qc = InventoryScreen::getQualityColor(quality);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(qc.x * 0.3f, qc.y * 0.3f, qc.z * 0.3f, 0.8f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(qc.x * 0.5f, qc.y * 0.5f, qc.z * 0.5f, 0.9f));
|
|
|
|
|
|
std::string lbl = item.stackCount > 1 ? std::to_string(item.stackCount) : ("##gi" + std::to_string(i));
|
|
|
|
|
|
if (ImGui::Button(lbl.c_str(), ImVec2(34, 34))) {
|
|
|
|
|
|
// Withdraw: auto-store to first free bag slot
|
|
|
|
|
|
gameHandler.guildBankWithdrawItem(activeTab, item.slotId, 0xFF, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
ImGui::TextColored(qc, "%s", name.c_str());
|
|
|
|
|
|
if (item.stackCount > 1) ImGui::Text("Count: %u", item.stackCount);
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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-02-25 14:44:44 -08:00
|
|
|
|
// Item tooltip on hover
|
|
|
|
|
|
if (ImGui::IsItemHovered() && info && info->valid) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
ImGui::TextColored(qc, "%s", info->name.c_str());
|
|
|
|
|
|
if (info->inventoryType > 0) {
|
|
|
|
|
|
if (!info->subclassName.empty())
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1), "%s", info->subclassName.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
if (info->armor > 0) ImGui::Text("%d Armor", info->armor);
|
|
|
|
|
|
if (info->damageMax > 0.0f && info->delayMs > 0) {
|
|
|
|
|
|
float speed = static_cast<float>(info->delayMs) / 1000.0f;
|
|
|
|
|
|
ImGui::Text("%.0f - %.0f Damage Speed %.2f", info->damageMin, info->damageMax, speed);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f);
|
|
|
|
|
|
std::string bonusLine;
|
|
|
|
|
|
auto appendStat = [](std::string& out, int32_t val, const char* n) {
|
|
|
|
|
|
if (val <= 0) return;
|
|
|
|
|
|
if (!out.empty()) out += " ";
|
|
|
|
|
|
out += "+" + std::to_string(val) + " " + n;
|
|
|
|
|
|
};
|
|
|
|
|
|
appendStat(bonusLine, info->strength, "Str");
|
|
|
|
|
|
appendStat(bonusLine, info->agility, "Agi");
|
|
|
|
|
|
appendStat(bonusLine, info->stamina, "Sta");
|
|
|
|
|
|
appendStat(bonusLine, info->intellect, "Int");
|
|
|
|
|
|
appendStat(bonusLine, info->spirit, "Spi");
|
|
|
|
|
|
if (!bonusLine.empty()) ImGui::TextColored(green, "%s", bonusLine.c_str());
|
|
|
|
|
|
if (info->sellPrice > 0) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1, 0.84f, 0, 1), "Sell: %ug %us %uc",
|
|
|
|
|
|
info->sellPrice / 10000, (info->sellPrice / 100) % 100, info->sellPrice % 100);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
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;
|
|
|
|
|
|
ImGui::Text("%ug%us%uc", bid / 10000, (bid / 100) % 100, bid % 100);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TableSetColumnIndex(4);
|
|
|
|
|
|
if (auction.buyoutPrice > 0) {
|
|
|
|
|
|
ImGui::Text("%ug%us%uc", auction.buyoutPrice / 10000,
|
|
|
|
|
|
(auction.buyoutPrice / 100) % 100, auction.buyoutPrice % 100);
|
|
|
|
|
|
} 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());
|
|
|
|
|
|
// Tooltip
|
|
|
|
|
|
if (ImGui::IsItemHovered() && info && info->valid) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
ImGui::TextColored(bqc, "%s", info->name.c_str());
|
|
|
|
|
|
if (info->armor > 0) ImGui::Text("%d Armor", info->armor);
|
|
|
|
|
|
if (info->damageMax > 0.0f && info->delayMs > 0) {
|
|
|
|
|
|
float speed = static_cast<float>(info->delayMs) / 1000.0f;
|
|
|
|
|
|
ImGui::Text("%.0f - %.0f Damage Speed %.2f", info->damageMin, info->damageMax, speed);
|
|
|
|
|
|
}
|
|
|
|
|
|
std::string bl;
|
|
|
|
|
|
auto appS = [](std::string& o, int32_t v, const char* n) {
|
|
|
|
|
|
if (v <= 0) return;
|
|
|
|
|
|
if (!o.empty()) o += " ";
|
|
|
|
|
|
o += "+" + std::to_string(v) + " " + n;
|
|
|
|
|
|
};
|
|
|
|
|
|
appS(bl, info->strength, "Str"); appS(bl, info->agility, "Agi");
|
|
|
|
|
|
appS(bl, info->stamina, "Sta"); appS(bl, info->intellect, "Int");
|
|
|
|
|
|
appS(bl, info->spirit, "Spi");
|
|
|
|
|
|
if (!bl.empty()) ImGui::TextColored(ImVec4(0,1,0,1), "%s", bl.c_str());
|
|
|
|
|
|
if (info->sellPrice > 0)
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1,0.84f,0,1), "Sell: %ug %us %uc",
|
|
|
|
|
|
info->sellPrice/10000, (info->sellPrice/100)%100, info->sellPrice%100);
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
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);
|
|
|
|
|
|
ImGui::Text("%ug%us%uc", a.currentBid / 10000, (a.currentBid / 100) % 100, a.currentBid % 100);
|
|
|
|
|
|
ImGui::TableSetColumnIndex(3);
|
|
|
|
|
|
if (a.buyoutPrice > 0)
|
|
|
|
|
|
ImGui::Text("%ug%us%uc", a.buyoutPrice / 10000, (a.buyoutPrice / 100) % 100, a.buyoutPrice % 100);
|
|
|
|
|
|
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());
|
|
|
|
|
|
if (ImGui::IsItemHovered() && info && info->valid) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
ImGui::TextColored(oqc, "%s", info->name.c_str());
|
|
|
|
|
|
if (info->armor > 0) ImGui::Text("%d Armor", info->armor);
|
|
|
|
|
|
if (info->damageMax > 0.0f && info->delayMs > 0) {
|
|
|
|
|
|
float speed = static_cast<float>(info->delayMs) / 1000.0f;
|
|
|
|
|
|
ImGui::Text("%.0f - %.0f Damage Speed %.2f", info->damageMin, info->damageMax, speed);
|
|
|
|
|
|
}
|
|
|
|
|
|
std::string ol;
|
|
|
|
|
|
auto appO = [](std::string& o, int32_t v, const char* n) {
|
|
|
|
|
|
if (v <= 0) return;
|
|
|
|
|
|
if (!o.empty()) o += " ";
|
|
|
|
|
|
o += "+" + std::to_string(v) + " " + n;
|
|
|
|
|
|
};
|
|
|
|
|
|
appO(ol, info->strength, "Str"); appO(ol, info->agility, "Agi");
|
|
|
|
|
|
appO(ol, info->stamina, "Sta"); appO(ol, info->intellect, "Int");
|
|
|
|
|
|
appO(ol, info->spirit, "Spi");
|
|
|
|
|
|
if (!ol.empty()) ImGui::TextColored(ImVec4(0,1,0,1), "%s", ol.c_str());
|
|
|
|
|
|
if (info->sellPrice > 0)
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1,0.84f,0,1), "Sell: %ug %us %uc",
|
|
|
|
|
|
info->sellPrice/10000, (info->sellPrice/100)%100, info->sellPrice%100);
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
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;
|
|
|
|
|
|
ImGui::Text("%ug%us%uc", bid / 10000, (bid / 100) % 100, bid % 100);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::TableSetColumnIndex(3);
|
|
|
|
|
|
if (a.buyoutPrice > 0)
|
|
|
|
|
|
ImGui::Text("%ug%us%uc", a.buyoutPrice / 10000, (a.buyoutPrice / 100) % 100, a.buyoutPrice % 100);
|
|
|
|
|
|
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
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::triggerDing(uint32_t newLevel) {
|
|
|
|
|
|
dingTimer_ = DING_DURATION;
|
|
|
|
|
|
dingLevel_ = newLevel;
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
float alpha = dingTimer_ < 0.8f ? (dingTimer_ / 0.8f) : 1.0f; // fade out last 0.8s
|
|
|
|
|
|
|
|
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
|
|
float cx = io.DisplaySize.x * 0.5f;
|
|
|
|
|
|
float cy = io.DisplaySize.y * 0.5f;
|
|
|
|
|
|
|
|
|
|
|
|
ImDrawList* draw = ImGui::GetForegroundDrawList();
|
|
|
|
|
|
|
|
|
|
|
|
// "LEVEL X!" text — visible for first 2.2s
|
|
|
|
|
|
if (dingTimer_ > 0.8f) {
|
|
|
|
|
|
ImFont* font = ImGui::GetFont();
|
|
|
|
|
|
float baseSize = ImGui::GetFontSize();
|
|
|
|
|
|
float fontSize = baseSize * 2.8f;
|
|
|
|
|
|
|
|
|
|
|
|
char buf[32];
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "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 - 20.0f;
|
|
|
|
|
|
|
|
|
|
|
|
// Drop shadow
|
|
|
|
|
|
draw->AddText(font, fontSize, ImVec2(tx + 3, ty + 3),
|
|
|
|
|
|
IM_COL32(0, 0, 0, (int)(alpha * 200)), buf);
|
|
|
|
|
|
// Gold text
|
|
|
|
|
|
draw->AddText(font, fontSize, ImVec2(tx, ty),
|
|
|
|
|
|
IM_COL32(255, 215, 0, (int)(alpha * 255)), buf);
|
|
|
|
|
|
|
|
|
|
|
|
// "DING!" subtitle
|
|
|
|
|
|
const char* ding = "DING!";
|
|
|
|
|
|
float dingSize = baseSize * 1.8f;
|
|
|
|
|
|
ImVec2 dingSz = font->CalcTextSizeA(dingSize, FLT_MAX, 0.0f, ding);
|
|
|
|
|
|
float dx = cx - dingSz.x * 0.5f;
|
|
|
|
|
|
float dy = ty + sz.y + 6.0f;
|
|
|
|
|
|
draw->AddText(font, dingSize, ImVec2(dx + 2, dy + 2),
|
|
|
|
|
|
IM_COL32(0, 0, 0, (int)(alpha * 180)), ding);
|
|
|
|
|
|
draw->AddText(font, dingSize, ImVec2(dx, dy),
|
|
|
|
|
|
IM_COL32(255, 255, 150, (int)(alpha * 255)), ding);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 13:53:42 -07:00
|
|
|
|
void GameScreen::triggerAchievementToast(uint32_t achievementId) {
|
|
|
|
|
|
achievementToastId_ = achievementId;
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
// Achievement ID line (until we have Achievement.dbc name lookup)
|
|
|
|
|
|
char idBuf[64];
|
|
|
|
|
|
std::snprintf(idBuf, sizeof(idBuf), "Achievement #%u", achievementToastId_);
|
|
|
|
|
|
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-09 13:47:07 -07:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Dungeon Finder window (toggle with hotkey or bag-bar button)
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
void GameScreen::renderDungeonFinderWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
// Toggle on I key when not typing
|
|
|
|
|
|
if (!chatInputActive && ImGui::IsKeyPressed(ImGuiKey_I, false)) {
|
|
|
|
|
|
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);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.3f, 0.9f, 0.3f, 1.0f), "Status: In queue (%d:%02d)", qMin, qSec);
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
case LfgState::Proposal:
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.1f, 1.0f), "Status: Group found!");
|
|
|
|
|
|
break;
|
|
|
|
|
|
case LfgState::Boot:
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Status: Vote kick in progress");
|
|
|
|
|
|
break;
|
|
|
|
|
|
case LfgState::InDungeon:
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "Status: In dungeon");
|
|
|
|
|
|
break;
|
|
|
|
|
|
case LfgState::FinishedDungeon:
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.6f, 1.0f, 0.6f, 1.0f), "Status: Dungeon complete");
|
|
|
|
|
|
break;
|
|
|
|
|
|
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) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.3f, 1.0f),
|
|
|
|
|
|
"A group has been found for your dungeon!");
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 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 {
|
|
|
|
|
|
// Build map name lookup from Map.dbc (cached after first call)
|
|
|
|
|
|
static std::unordered_map<uint32_t, std::string> sMapNames;
|
|
|
|
|
|
static bool sMapNamesLoaded = false;
|
|
|
|
|
|
if (!sMapNamesLoaded) {
|
|
|
|
|
|
sMapNamesLoaded = true;
|
|
|
|
|
|
if (auto* am = core::Application::getInstance().getAssetManager()) {
|
|
|
|
|
|
if (auto dbc = am->loadDBC("Map.dbc"); dbc && dbc->isLoaded()) {
|
|
|
|
|
|
for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) {
|
|
|
|
|
|
uint32_t id = dbc->getUInt32(i, 0);
|
|
|
|
|
|
// Field 2 = MapName_enUS (first localized), field 1 = InternalName
|
|
|
|
|
|
std::string name = dbc->getString(i, 2);
|
|
|
|
|
|
if (name.empty()) name = dbc->getString(i, 1);
|
|
|
|
|
|
if (!name.empty()) sMapNames[id] = std::move(name);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
|
|
// Instance name
|
|
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
|
|
|
|
|
auto it = sMapNames.find(lo.mapId);
|
|
|
|
|
|
if (it != sMapNames.end()) {
|
|
|
|
|
|
ImGui::TextUnformatted(it->second.c_str());
|
|
|
|
|
|
} 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-02-02 12:24:50 -08:00
|
|
|
|
}} // namespace wowee::ui
|