mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-26 13:13:50 +00:00
- Extract ChatPanel monolith into 15+ focused modules under ui/chat/ (ChatInput, ChatTabManager, ChatTabCompleter, ChatMarkupParser, ChatMarkupRenderer, ChatCommandRegistry, ChatBubbleManager, ChatSettings, MacroEvaluator, GameStateAdapter, InputModifierAdapter) - Split 2700-line chat_panel_commands.cpp into 11 command modules - Add GM command handling: 190-command data table, dot-prefix interception, tab-completion, /gmhelp with category filter - Fix ChatType enum to match WoW wire protocol (SAY=0x01 not 0x00); values 0x00-0x1B shared across Vanilla/TBC/WotLK - Fix BG_SYSTEM_* values from 82-84 (UB in bitmask shifts) to 0x24-0x26 - Fix infinite Enter key loop after teleport (disable TOGGLE_CHAT repeat, add 2-frame input cooldown) - Add tests: chat_markup_parser, chat_tab_completer, gm_commands, macro_evaluator Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
1421 lines
68 KiB
C++
1421 lines
68 KiB
C++
#include "ui/game_screen.hpp"
|
|
#include "ui/ui_colors.hpp"
|
|
#include "ui/ui_helpers.hpp"
|
|
#include "rendering/vk_context.hpp"
|
|
#include "core/application.hpp"
|
|
#include "core/appearance_composer.hpp"
|
|
#include "addons/addon_manager.hpp"
|
|
#include "core/coordinates.hpp"
|
|
#include "core/input.hpp"
|
|
#include "rendering/renderer.hpp"
|
|
#include "rendering/post_process_pipeline.hpp"
|
|
#include "rendering/animation_controller.hpp"
|
|
#include "rendering/wmo_renderer.hpp"
|
|
#include "rendering/terrain_manager.hpp"
|
|
#include "rendering/minimap.hpp"
|
|
#include "rendering/world_map.hpp"
|
|
#include "rendering/character_renderer.hpp"
|
|
#include "rendering/camera.hpp"
|
|
#include "rendering/camera_controller.hpp"
|
|
#include "audio/audio_coordinator.hpp"
|
|
#include "audio/audio_engine.hpp"
|
|
#include "audio/music_manager.hpp"
|
|
#include "game/zone_manager.hpp"
|
|
#include "audio/footstep_manager.hpp"
|
|
#include "audio/activity_sound_manager.hpp"
|
|
#include "audio/mount_sound_manager.hpp"
|
|
#include "audio/npc_voice_manager.hpp"
|
|
#include "audio/ambient_sound_manager.hpp"
|
|
#include "audio/ui_sound_manager.hpp"
|
|
#include "audio/combat_sound_manager.hpp"
|
|
#include "audio/spell_sound_manager.hpp"
|
|
#include "audio/movement_sound_manager.hpp"
|
|
#include "pipeline/asset_manager.hpp"
|
|
#include "pipeline/dbc_loader.hpp"
|
|
#include "pipeline/dbc_layout.hpp"
|
|
|
|
#include "game/expansion_profile.hpp"
|
|
#include "game/character.hpp"
|
|
#include "core/logger.hpp"
|
|
#include <imgui.h>
|
|
#include <imgui_internal.h>
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <sstream>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <cctype>
|
|
#include <chrono>
|
|
#include <ctime>
|
|
|
|
#include <unordered_set>
|
|
|
|
namespace {
|
|
using namespace wowee::ui::colors;
|
|
using namespace wowee::ui::helpers;
|
|
constexpr auto& kColorRed = kRed;
|
|
constexpr auto& kColorGreen = kGreen;
|
|
constexpr auto& kColorBrightGreen= kBrightGreen;
|
|
constexpr auto& kColorYellow = kYellow;
|
|
constexpr auto& kColorGray = kGray;
|
|
constexpr auto& kColorDarkGray = kDarkGray;
|
|
|
|
// Abbreviated month names (indexed 0-11)
|
|
constexpr const char* kMonthAbbrev[12] = {
|
|
"Jan","Feb","Mar","Apr","May","Jun",
|
|
"Jul","Aug","Sep","Oct","Nov","Dec"
|
|
};
|
|
|
|
// Common ImGui window flags for popup dialogs
|
|
const ImGuiWindowFlags kDialogFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize;
|
|
|
|
bool raySphereIntersect(const wowee::rendering::Ray& ray, const glm::vec3& center, float radius, float& tOut) {
|
|
glm::vec3 oc = ray.origin - center;
|
|
float b = glm::dot(oc, ray.direction);
|
|
float c = glm::dot(oc, oc) - radius * radius;
|
|
float discriminant = b * b - c;
|
|
if (discriminant < 0.0f) return false;
|
|
float t = -b - std::sqrt(discriminant);
|
|
if (t < 0.0f) t = -b + std::sqrt(discriminant);
|
|
if (t < 0.0f) return false;
|
|
tOut = t;
|
|
return true;
|
|
}
|
|
|
|
std::string getEntityName(const std::shared_ptr<wowee::game::Entity>& entity) {
|
|
if (entity->getType() == wowee::game::ObjectType::PLAYER) {
|
|
auto player = std::static_pointer_cast<wowee::game::Player>(entity);
|
|
if (!player->getName().empty()) return player->getName();
|
|
} else if (entity->getType() == wowee::game::ObjectType::UNIT) {
|
|
auto unit = std::static_pointer_cast<wowee::game::Unit>(entity);
|
|
if (!unit->getName().empty()) return unit->getName();
|
|
} else if (entity->getType() == wowee::game::ObjectType::GAMEOBJECT) {
|
|
auto go = std::static_pointer_cast<wowee::game::GameObject>(entity);
|
|
if (!go->getName().empty()) return go->getName();
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
// Draw a four-edge screen vignette (gradient overlay along each edge).
|
|
// Used for damage flash, low-health pulse, and level-up golden burst.
|
|
void drawScreenEdgeVignette(uint8_t r, uint8_t g, uint8_t b,
|
|
int alpha, float thicknessRatio) {
|
|
if (alpha <= 0) return;
|
|
ImDrawList* fg = ImGui::GetForegroundDrawList();
|
|
const float W = ImGui::GetIO().DisplaySize.x;
|
|
const float H = ImGui::GetIO().DisplaySize.y;
|
|
const float thickness = std::min(W, H) * thicknessRatio;
|
|
const ImU32 edgeCol = IM_COL32(r, g, b, alpha);
|
|
const ImU32 fadeCol = IM_COL32(r, g, b, 0);
|
|
// Top
|
|
fg->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(W, thickness),
|
|
edgeCol, edgeCol, fadeCol, fadeCol);
|
|
// Bottom
|
|
fg->AddRectFilledMultiColor(ImVec2(0, H - thickness), ImVec2(W, H),
|
|
fadeCol, fadeCol, edgeCol, edgeCol);
|
|
// Left
|
|
fg->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(thickness, H),
|
|
edgeCol, fadeCol, fadeCol, edgeCol);
|
|
// Right
|
|
fg->AddRectFilledMultiColor(ImVec2(W - thickness, 0), ImVec2(W, H),
|
|
fadeCol, edgeCol, edgeCol, fadeCol);
|
|
}
|
|
|
|
}
|
|
|
|
namespace wowee { namespace ui {
|
|
|
|
GameScreen::GameScreen() {
|
|
loadSettings();
|
|
}
|
|
|
|
// Set UI services and propagate to child components
|
|
void GameScreen::setServices(const UIServices& services) {
|
|
services_ = services;
|
|
// Update legacy pointer for compatibility
|
|
appearanceComposer_ = services.appearanceComposer;
|
|
// Propagate to child panels
|
|
chatPanel_.setServices(services);
|
|
toastManager_.setServices(services);
|
|
dialogManager_.setServices(services);
|
|
settingsPanel_.setServices(services);
|
|
combatUI_.setServices(services);
|
|
socialPanel_.setServices(services);
|
|
actionBarPanel_.setServices(services);
|
|
windowManager_.setServices(services);
|
|
}
|
|
|
|
void GameScreen::render(game::GameHandler& gameHandler) {
|
|
// Set up chat bubble callback (once) and cache game handler in ChatPanel
|
|
chatPanel_.setupCallbacks(gameHandler);
|
|
toastManager_.setupCallbacks(gameHandler);
|
|
|
|
// Set up appearance-changed callback to refresh inventory preview (barber shop, etc.)
|
|
if (!appearanceCallbackSet_) {
|
|
gameHandler.setAppearanceChangedCallback([this]() {
|
|
inventoryScreenCharGuid_ = 0; // force preview re-sync on next frame
|
|
});
|
|
appearanceCallbackSet_ = true;
|
|
}
|
|
|
|
// Set up UI error frame callback (once)
|
|
if (!uiErrorCallbackSet_) {
|
|
gameHandler.setUIErrorCallback([this](const std::string& msg) {
|
|
uiErrors_.push_back({msg, 0.0f});
|
|
if (uiErrors_.size() > 5) uiErrors_.erase(uiErrors_.begin());
|
|
// Play error sound for each new error (rate-limited by deque cap of 5)
|
|
if (auto* ac = services_.audioCoordinator) {
|
|
if (auto* sfx = ac->getUiSoundManager()) sfx->playError();
|
|
}
|
|
});
|
|
uiErrorCallbackSet_ = true;
|
|
}
|
|
|
|
// Flash the action bar button whose spell just failed (0.5 s red overlay).
|
|
if (!castFailedCallbackSet_) {
|
|
gameHandler.setSpellCastFailedCallback([this](uint32_t spellId) {
|
|
if (spellId == 0) return;
|
|
float now = static_cast<float>(ImGui::GetTime());
|
|
actionBarPanel_.actionFlashEndTimes_[spellId] = now + actionBarPanel_.kActionFlashDuration;
|
|
});
|
|
castFailedCallbackSet_ = true;
|
|
}
|
|
|
|
// Apply UI transparency setting
|
|
float prevAlpha = ImGui::GetStyle().Alpha;
|
|
ImGui::GetStyle().Alpha = settingsPanel_.uiOpacity_;
|
|
|
|
// Sync minimap opacity with UI opacity
|
|
{
|
|
auto* renderer = services_.renderer;
|
|
if (renderer) {
|
|
if (auto* minimap = renderer->getMinimap()) {
|
|
minimap->setOpacity(settingsPanel_.uiOpacity_);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply initial settings when renderer becomes available
|
|
if (!settingsPanel_.minimapSettingsApplied_) {
|
|
auto* renderer = services_.renderer;
|
|
if (renderer) {
|
|
if (auto* minimap = renderer->getMinimap()) {
|
|
settingsPanel_.minimapRotate_ = false;
|
|
settingsPanel_.pendingMinimapRotate = false;
|
|
minimap->setRotateWithCamera(false);
|
|
minimap->setSquareShape(settingsPanel_.minimapSquare_);
|
|
settingsPanel_.minimapSettingsApplied_ = true;
|
|
}
|
|
if (auto* zm = renderer->getZoneManager()) {
|
|
zm->setUseOriginalSoundtrack(settingsPanel_.pendingUseOriginalSoundtrack);
|
|
}
|
|
if (auto* tm = renderer->getTerrainManager()) {
|
|
tm->setGroundClutterDensityScale(static_cast<float>(settingsPanel_.pendingGroundClutterDensity) / 100.0f);
|
|
}
|
|
// Restore mute state: save actual master volume first, then apply mute
|
|
if (settingsPanel_.soundMuted_) {
|
|
float actual = audio::AudioEngine::instance().getMasterVolume();
|
|
settingsPanel_.preMuteVolume_ = (actual > 0.0f) ? actual
|
|
: static_cast<float>(settingsPanel_.pendingMasterVolume) / 100.0f;
|
|
audio::AudioEngine::instance().setMasterVolume(0.0f);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply saved volume settings once when audio managers first become available
|
|
if (!settingsPanel_.volumeSettingsApplied_) {
|
|
auto* ac = services_.audioCoordinator;
|
|
if (ac && ac->getUiSoundManager()) {
|
|
settingsPanel_.applyAudioVolumes(ac);
|
|
settingsPanel_.volumeSettingsApplied_ = true;
|
|
}
|
|
}
|
|
|
|
// Apply saved MSAA setting once when renderer is available
|
|
if (!settingsPanel_.msaaSettingsApplied_ && settingsPanel_.pendingAntiAliasing > 0) {
|
|
auto* renderer = services_.renderer;
|
|
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[settingsPanel_.pendingAntiAliasing]);
|
|
settingsPanel_.msaaSettingsApplied_ = true;
|
|
}
|
|
} else {
|
|
settingsPanel_.msaaSettingsApplied_ = true;
|
|
}
|
|
|
|
// Apply saved FXAA setting once when renderer is available
|
|
if (!settingsPanel_.fxaaSettingsApplied_) {
|
|
auto* renderer = services_.renderer;
|
|
if (renderer) {
|
|
renderer->getPostProcessPipeline()->setFXAAEnabled(settingsPanel_.pendingFXAA);
|
|
settingsPanel_.fxaaSettingsApplied_ = true;
|
|
}
|
|
}
|
|
|
|
// Apply saved water refraction setting once when renderer is available
|
|
if (!settingsPanel_.waterRefractionApplied_) {
|
|
auto* renderer = services_.renderer;
|
|
if (renderer) {
|
|
renderer->setWaterRefractionEnabled(settingsPanel_.pendingWaterRefraction);
|
|
settingsPanel_.waterRefractionApplied_ = true;
|
|
}
|
|
}
|
|
|
|
// Apply saved normal mapping / POM settings once when WMO renderer is available
|
|
if (!settingsPanel_.normalMapSettingsApplied_) {
|
|
auto* renderer = services_.renderer;
|
|
if (renderer) {
|
|
if (auto* wr = renderer->getWMORenderer()) {
|
|
wr->setNormalMappingEnabled(settingsPanel_.pendingNormalMapping);
|
|
wr->setNormalMapStrength(settingsPanel_.pendingNormalMapStrength);
|
|
wr->setPOMEnabled(settingsPanel_.pendingPOM);
|
|
wr->setPOMQuality(settingsPanel_.pendingPOMQuality);
|
|
if (auto* cr = renderer->getCharacterRenderer()) {
|
|
cr->setNormalMappingEnabled(settingsPanel_.pendingNormalMapping);
|
|
cr->setNormalMapStrength(settingsPanel_.pendingNormalMapStrength);
|
|
cr->setPOMEnabled(settingsPanel_.pendingPOM);
|
|
cr->setPOMQuality(settingsPanel_.pendingPOMQuality);
|
|
}
|
|
settingsPanel_.normalMapSettingsApplied_ = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply saved upscaling setting once when renderer is available
|
|
if (!settingsPanel_.fsrSettingsApplied_) {
|
|
auto* renderer = services_.renderer;
|
|
if (renderer) {
|
|
static constexpr float fsrScales[] = { 0.77f, 0.67f, 0.59f, 1.00f };
|
|
settingsPanel_.pendingFSRQuality = std::clamp(settingsPanel_.pendingFSRQuality, 0, 3);
|
|
renderer->getPostProcessPipeline()->setFSRQuality(fsrScales[settingsPanel_.pendingFSRQuality]);
|
|
renderer->getPostProcessPipeline()->setFSRSharpness(settingsPanel_.pendingFSRSharpness);
|
|
renderer->getPostProcessPipeline()->setFSR2DebugTuning(settingsPanel_.pendingFSR2JitterSign, settingsPanel_.pendingFSR2MotionVecScaleX, settingsPanel_.pendingFSR2MotionVecScaleY);
|
|
renderer->getPostProcessPipeline()->setAmdFsr3FramegenEnabled(settingsPanel_.pendingAMDFramegen);
|
|
int effectiveMode = settingsPanel_.pendingUpscalingMode;
|
|
|
|
// Defer FSR2/FSR3 activation until fully in-world to avoid
|
|
// init issues during login/character selection screens.
|
|
if (effectiveMode == 2 && gameHandler.getState() != game::WorldState::IN_WORLD) {
|
|
renderer->setFSREnabled(false);
|
|
renderer->setFSR2Enabled(false);
|
|
} else {
|
|
renderer->setFSREnabled(effectiveMode == 1);
|
|
renderer->setFSR2Enabled(effectiveMode == 2);
|
|
settingsPanel_.fsrSettingsApplied_ = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply auto-loot / auto-sell settings to GameHandler every frame (cheap bool sync)
|
|
gameHandler.setAutoLoot(settingsPanel_.pendingAutoLoot);
|
|
gameHandler.setAutoSellGrey(settingsPanel_.pendingAutoSellGrey);
|
|
gameHandler.setAutoRepair(settingsPanel_.pendingAutoRepair);
|
|
|
|
// Sync chat auto-join settings to GameHandler
|
|
gameHandler.chatAutoJoin.general = chatPanel_.chatAutoJoinGeneral;
|
|
gameHandler.chatAutoJoin.trade = chatPanel_.chatAutoJoinTrade;
|
|
gameHandler.chatAutoJoin.localDefense = chatPanel_.chatAutoJoinLocalDefense;
|
|
gameHandler.chatAutoJoin.lfg = chatPanel_.chatAutoJoinLFG;
|
|
gameHandler.chatAutoJoin.local = chatPanel_.chatAutoJoinLocal;
|
|
|
|
// Process targeting input before UI windows
|
|
processTargetInput(gameHandler);
|
|
|
|
renderPlayerFrame(gameHandler);
|
|
|
|
// Pet frame (below player frame, only when player has an active pet)
|
|
if (gameHandler.hasPet()) {
|
|
renderPetFrame(gameHandler);
|
|
}
|
|
|
|
// Auto-open pet rename modal when server signals the pet is renameable (first tame)
|
|
if (gameHandler.consumePetRenameablePending()) {
|
|
petRenameOpen_ = true;
|
|
petRenameBuf_[0] = '\0';
|
|
}
|
|
|
|
// Totem frame (Shaman only, when any totem is active)
|
|
if (gameHandler.getPlayerClass() == 7) {
|
|
renderTotemFrame(gameHandler);
|
|
}
|
|
|
|
// Target frame (only when we have a target)
|
|
if (gameHandler.hasTarget()) {
|
|
renderTargetFrame(gameHandler);
|
|
}
|
|
|
|
// Focus target frame (only when we have a focus)
|
|
if (gameHandler.hasFocus()) {
|
|
renderFocusFrame(gameHandler);
|
|
}
|
|
|
|
// Render windows
|
|
if (showPlayerInfo) {
|
|
renderPlayerInfo(gameHandler);
|
|
}
|
|
|
|
if (showEntityWindow) {
|
|
renderEntityList(gameHandler);
|
|
}
|
|
|
|
if (showChatWindow) {
|
|
chatPanel_.getSpellIcon = [this](uint32_t id, pipeline::AssetManager* am) {
|
|
return getSpellIcon(id, am);
|
|
};
|
|
chatPanel_.render(gameHandler, inventoryScreen, spellbookScreen, questLogScreen);
|
|
// Process slash commands that affect GameScreen state
|
|
auto cmds = chatPanel_.consumeSlashCommands();
|
|
if (cmds.showInspect) socialPanel_.showInspectWindow_ = true;
|
|
if (cmds.toggleThreat) combatUI_.showThreatWindow_ = !combatUI_.showThreatWindow_;
|
|
if (cmds.showBgScore) combatUI_.showBgScoreboard_ = !combatUI_.showBgScoreboard_;
|
|
if (cmds.showGmTicket) windowManager_.showGmTicketWindow_ = true;
|
|
if (cmds.showWho) socialPanel_.showWhoWindow_ = true;
|
|
if (cmds.toggleCombatLog) combatUI_.showCombatLog_ = !combatUI_.showCombatLog_;
|
|
if (cmds.takeScreenshot) takeScreenshot(gameHandler);
|
|
}
|
|
|
|
// ---- New UI elements ----
|
|
actionBarPanel_.renderActionBar(gameHandler, settingsPanel_, chatPanel_,
|
|
inventoryScreen, spellbookScreen, questLogScreen,
|
|
[this](uint32_t id, pipeline::AssetManager* am) { return getSpellIcon(id, am); });
|
|
actionBarPanel_.renderStanceBar(gameHandler, settingsPanel_, spellbookScreen,
|
|
[this](uint32_t id, pipeline::AssetManager* am) { return getSpellIcon(id, am); });
|
|
actionBarPanel_.renderBagBar(gameHandler, settingsPanel_, inventoryScreen);
|
|
actionBarPanel_.renderXpBar(gameHandler, settingsPanel_);
|
|
actionBarPanel_.renderRepBar(gameHandler, settingsPanel_);
|
|
auto spellIconFn = [this](uint32_t id, pipeline::AssetManager* am) { return getSpellIcon(id, am); };
|
|
combatUI_.renderCastBar(gameHandler, spellIconFn);
|
|
renderMirrorTimers(gameHandler);
|
|
combatUI_.renderCooldownTracker(gameHandler, settingsPanel_, spellIconFn);
|
|
renderQuestObjectiveTracker(gameHandler);
|
|
renderNameplates(gameHandler); // player names always shown; NPC plates gated by showNameplates_
|
|
combatUI_.renderBattlegroundScore(gameHandler);
|
|
combatUI_.renderRaidWarningOverlay(gameHandler);
|
|
combatUI_.renderCombatText(gameHandler);
|
|
combatUI_.renderDPSMeter(gameHandler, settingsPanel_);
|
|
renderDurabilityWarning(gameHandler);
|
|
renderUIErrors(gameHandler, ImGui::GetIO().DeltaTime);
|
|
toastManager_.renderEarlyToasts(ImGui::GetIO().DeltaTime, gameHandler);
|
|
if (socialPanel_.showRaidFrames_) {
|
|
socialPanel_.renderPartyFrames(gameHandler, chatPanel_, spellIconFn);
|
|
}
|
|
socialPanel_.renderBossFrames(gameHandler, spellbookScreen, spellIconFn);
|
|
dialogManager_.renderDialogs(gameHandler, inventoryScreen, chatPanel_);
|
|
socialPanel_.renderGuildRoster(gameHandler, chatPanel_);
|
|
socialPanel_.renderSocialFrame(gameHandler, chatPanel_);
|
|
combatUI_.renderBuffBar(gameHandler, spellbookScreen, spellIconFn);
|
|
windowManager_.renderLootWindow(gameHandler, inventoryScreen, chatPanel_);
|
|
windowManager_.renderGossipWindow(gameHandler, chatPanel_);
|
|
windowManager_.renderQuestDetailsWindow(gameHandler, chatPanel_, inventoryScreen);
|
|
windowManager_.renderQuestRequestItemsWindow(gameHandler, chatPanel_, inventoryScreen);
|
|
windowManager_.renderQuestOfferRewardWindow(gameHandler, chatPanel_, inventoryScreen);
|
|
windowManager_.renderVendorWindow(gameHandler, inventoryScreen, chatPanel_);
|
|
windowManager_.renderTrainerWindow(gameHandler,
|
|
[this](uint32_t id, pipeline::AssetManager* am) { return getSpellIcon(id, am); });
|
|
windowManager_.renderBarberShopWindow(gameHandler);
|
|
windowManager_.renderStableWindow(gameHandler);
|
|
windowManager_.renderTaxiWindow(gameHandler);
|
|
windowManager_.renderMailWindow(gameHandler, inventoryScreen, chatPanel_);
|
|
windowManager_.renderMailComposeWindow(gameHandler, inventoryScreen);
|
|
windowManager_.renderBankWindow(gameHandler, inventoryScreen, chatPanel_);
|
|
windowManager_.renderGuildBankWindow(gameHandler, inventoryScreen, chatPanel_);
|
|
windowManager_.renderAuctionHouseWindow(gameHandler, inventoryScreen, chatPanel_);
|
|
socialPanel_.renderDungeonFinderWindow(gameHandler, chatPanel_);
|
|
windowManager_.renderInstanceLockouts(gameHandler);
|
|
socialPanel_.renderWhoWindow(gameHandler, chatPanel_);
|
|
combatUI_.renderCombatLog(gameHandler, spellbookScreen);
|
|
windowManager_.renderAchievementWindow(gameHandler);
|
|
windowManager_.renderSkillsWindow(gameHandler);
|
|
windowManager_.renderTitlesWindow(gameHandler);
|
|
windowManager_.renderEquipSetWindow(gameHandler);
|
|
windowManager_.renderGmTicketWindow(gameHandler);
|
|
socialPanel_.renderInspectWindow(gameHandler, inventoryScreen);
|
|
windowManager_.renderBookWindow(gameHandler);
|
|
combatUI_.renderThreatWindow(gameHandler);
|
|
combatUI_.renderBgScoreboard(gameHandler);
|
|
if (showMinimap_) {
|
|
renderMinimapMarkers(gameHandler);
|
|
}
|
|
windowManager_.renderLogoutCountdown(gameHandler);
|
|
windowManager_.renderDeathScreen(gameHandler);
|
|
windowManager_.renderReclaimCorpseButton(gameHandler);
|
|
dialogManager_.renderLateDialogs(gameHandler);
|
|
chatPanel_.renderBubbles(gameHandler);
|
|
windowManager_.renderEscapeMenu(settingsPanel_);
|
|
settingsPanel_.renderSettingsWindow(inventoryScreen, chatPanel_, [this]() { saveSettings(); });
|
|
toastManager_.renderLateToasts(gameHandler);
|
|
renderWeatherOverlay(gameHandler);
|
|
|
|
renderWorldMap(gameHandler);
|
|
|
|
questLogScreen.render(gameHandler, inventoryScreen);
|
|
|
|
spellbookScreen.render(gameHandler, services_.assetManager);
|
|
|
|
// Insert spell link into chat if player shift-clicked a spellbook entry
|
|
{
|
|
std::string pendingSpellLink = spellbookScreen.getAndClearPendingChatLink();
|
|
if (!pendingSpellLink.empty()) {
|
|
chatPanel_.insertChatLink(pendingSpellLink);
|
|
}
|
|
}
|
|
|
|
// Talents (N key toggle handled inside)
|
|
talentScreen.render(gameHandler);
|
|
|
|
// Set up inventory screen asset manager + player appearance (re-init on character switch)
|
|
{
|
|
uint64_t activeGuid = gameHandler.getActiveCharacterGuid();
|
|
if (activeGuid != 0 && activeGuid != inventoryScreenCharGuid_) {
|
|
auto* am = services_.assetManager;
|
|
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);
|
|
inventoryScreenCharGuid_ = activeGuid;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set vendor mode before rendering inventory
|
|
inventoryScreen.setVendorMode(gameHandler.isVendorWindowOpen(), &gameHandler);
|
|
|
|
// Auto-open bags once when vendor window first opens
|
|
if (gameHandler.isVendorWindowOpen()) {
|
|
if (!windowManager_.vendorBagsOpened_) {
|
|
windowManager_.vendorBagsOpened_ = true;
|
|
if (inventoryScreen.isSeparateBags()) {
|
|
inventoryScreen.openAllBags();
|
|
} else if (!inventoryScreen.isOpen()) {
|
|
inventoryScreen.setOpen(true);
|
|
}
|
|
}
|
|
} else {
|
|
windowManager_.vendorBagsOpened_ = false;
|
|
}
|
|
|
|
inventoryScreen.setGameHandler(&gameHandler);
|
|
inventoryScreen.render(gameHandler.getInventory(), gameHandler.getMoneyCopper());
|
|
|
|
// Character screen (C key toggle handled inside render())
|
|
inventoryScreen.renderCharacterScreen(gameHandler);
|
|
|
|
// Insert item link into chat if player shift-clicked any inventory/equipment slot
|
|
{
|
|
std::string pendingLink = inventoryScreen.getAndClearPendingChatLink();
|
|
if (!pendingLink.empty()) {
|
|
chatPanel_.insertChatLink(pendingLink);
|
|
}
|
|
}
|
|
|
|
if (inventoryScreen.consumeEquipmentDirty() || gameHandler.consumeOnlineEquipmentDirty()) {
|
|
updateCharacterGeosets(gameHandler.getInventory());
|
|
updateCharacterTextures(gameHandler.getInventory());
|
|
if (appearanceComposer_) appearanceComposer_->loadEquippedWeapons();
|
|
inventoryScreen.markPreviewDirty();
|
|
// Update renderer weapon type for animation selection
|
|
auto* r = services_.renderer;
|
|
if (r) {
|
|
const auto& mh = gameHandler.getInventory().getEquipSlot(game::EquipSlot::MAIN_HAND);
|
|
const auto& oh = gameHandler.getInventory().getEquipSlot(game::EquipSlot::OFF_HAND);
|
|
if (mh.empty()) {
|
|
if (auto* ac = r->getAnimationController()) ac->setEquippedWeaponType(0, false);
|
|
} else {
|
|
// Polearms and staves use ATTACK_2H_LOOSE instead of ATTACK_2H
|
|
bool is2HLoose = (mh.item.subclassName == "Polearm" || mh.item.subclassName == "Staff");
|
|
bool isFist = (mh.item.subclassName == "Fist Weapon");
|
|
bool isDagger = (mh.item.subclassName == "Dagger");
|
|
bool hasOffHand = !oh.empty() &&
|
|
(oh.item.inventoryType == game::InvType::ONE_HAND ||
|
|
oh.item.subclassName == "Fist Weapon");
|
|
bool hasShield = !oh.empty() && oh.item.inventoryType == game::InvType::SHIELD;
|
|
if (auto* ac = r->getAnimationController()) ac->setEquippedWeaponType(mh.item.inventoryType, is2HLoose, isFist, isDagger, hasOffHand, hasShield);
|
|
}
|
|
// Detect ranged weapon type from RANGED slot
|
|
const auto& rangedSlot = gameHandler.getInventory().getEquipSlot(game::EquipSlot::RANGED);
|
|
if (rangedSlot.empty()) {
|
|
if (auto* ac = r->getAnimationController()) ac->setEquippedRangedType(rendering::RangedWeaponType::NONE);
|
|
} else if (rangedSlot.item.inventoryType == game::InvType::RANGED_BOW) {
|
|
// subclassName distinguishes Bow vs Crossbow
|
|
if (rangedSlot.item.subclassName == "Crossbow") {
|
|
if (auto* ac = r->getAnimationController()) ac->setEquippedRangedType(rendering::RangedWeaponType::CROSSBOW);
|
|
} else {
|
|
if (auto* ac = r->getAnimationController()) ac->setEquippedRangedType(rendering::RangedWeaponType::BOW);
|
|
}
|
|
} else if (rangedSlot.item.inventoryType == game::InvType::RANGED_GUN) {
|
|
if (auto* ac = r->getAnimationController()) ac->setEquippedRangedType(rendering::RangedWeaponType::GUN);
|
|
} else if (rangedSlot.item.inventoryType == game::InvType::THROWN) {
|
|
if (auto* ac = r->getAnimationController()) ac->setEquippedRangedType(rendering::RangedWeaponType::THROWN);
|
|
} else {
|
|
if (auto* ac = r->getAnimationController()) ac->setEquippedRangedType(rendering::RangedWeaponType::NONE);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update renderer face-target position and selection circle
|
|
auto* renderer = services_.renderer;
|
|
if (renderer) {
|
|
if (auto* ac = renderer->getAnimationController()) ac->setInCombat(gameHandler.isInCombat() &&
|
|
!gameHandler.isPlayerDead() &&
|
|
!gameHandler.isPlayerGhost());
|
|
if (auto* cr = renderer->getCharacterRenderer()) {
|
|
uint32_t charInstId = renderer->getCharacterInstanceId();
|
|
if (charInstId != 0) {
|
|
const bool isGhost = gameHandler.isPlayerGhost();
|
|
if (!ghostOpacityStateKnown_ ||
|
|
ghostOpacityLastState_ != isGhost ||
|
|
ghostOpacityLastInstanceId_ != charInstId) {
|
|
cr->setInstanceOpacity(charInstId, isGhost ? 0.5f : 1.0f);
|
|
ghostOpacityStateKnown_ = true;
|
|
ghostOpacityLastState_ = isGhost;
|
|
ghostOpacityLastInstanceId_ = charInstId;
|
|
}
|
|
}
|
|
}
|
|
static glm::vec3 targetGLPos;
|
|
if (gameHandler.hasTarget()) {
|
|
auto target = gameHandler.getTarget();
|
|
if (target) {
|
|
// Prefer the renderer's actual instance position so the selection
|
|
// circle tracks the rendered model (not a parallel entity-space
|
|
// interpolator that can drift from the visual position).
|
|
glm::vec3 instPos;
|
|
if (core::Application::getInstance().getRenderPositionForGuid(target->getGuid(), instPos)) {
|
|
targetGLPos = instPos;
|
|
// Override Z with foot position to sit the circle on the ground.
|
|
float footZ = 0.0f;
|
|
if (core::Application::getInstance().getRenderFootZForGuid(target->getGuid(), footZ)) {
|
|
targetGLPos.z = footZ;
|
|
}
|
|
} else {
|
|
// Fallback: entity game-logic position (no CharacterRenderer instance yet)
|
|
targetGLPos = core::coords::canonicalToRender(
|
|
glm::vec3(target->getX(), target->getY(), target->getZ()));
|
|
float footZ = 0.0f;
|
|
if (core::Application::getInstance().getRenderFootZForGuid(target->getGuid(), footZ)) {
|
|
targetGLPos.z = footZ;
|
|
}
|
|
}
|
|
if (auto* ac = renderer->getAnimationController()) ac->setTargetPosition(&targetGLPos);
|
|
|
|
// Selection circle color: WoW-canonical level-based colors
|
|
bool showSelectionCircle = false;
|
|
glm::vec3 circleColor(1.0f, 1.0f, 0.3f); // default yellow
|
|
float circleRadius = 1.5f;
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
if (target->getType() == game::ObjectType::UNIT) {
|
|
showSelectionCircle = true;
|
|
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)
|
|
} else if (unit->isHostile() || gameHandler.isAggressiveTowardPlayer(target->getGuid())) {
|
|
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
|
|
}
|
|
} else {
|
|
circleColor = glm::vec3(0.3f, 1.0f, 0.3f); // green (friendly)
|
|
}
|
|
} else if (target->getType() == game::ObjectType::PLAYER) {
|
|
showSelectionCircle = true;
|
|
circleColor = glm::vec3(0.3f, 1.0f, 0.3f); // green (player)
|
|
}
|
|
if (showSelectionCircle) {
|
|
renderer->setSelectionCircle(targetGLPos, circleRadius, circleColor);
|
|
} else {
|
|
renderer->clearSelectionCircle();
|
|
}
|
|
} else {
|
|
if (auto* ac = renderer->getAnimationController()) ac->setTargetPosition(nullptr);
|
|
renderer->clearSelectionCircle();
|
|
}
|
|
} else {
|
|
if (auto* ac = renderer->getAnimationController()) ac->setTargetPosition(nullptr);
|
|
renderer->clearSelectionCircle();
|
|
}
|
|
}
|
|
|
|
// Screen edge damage flash — red vignette that fires on HP decrease
|
|
{
|
|
auto playerEntity = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid());
|
|
uint32_t currentHp = 0;
|
|
if (playerEntity && (playerEntity->getType() == game::ObjectType::PLAYER ||
|
|
playerEntity->getType() == game::ObjectType::UNIT)) {
|
|
auto unit = std::static_pointer_cast<game::Unit>(playerEntity);
|
|
if (unit->getMaxHealth() > 0)
|
|
currentHp = unit->getHealth();
|
|
}
|
|
|
|
// Detect HP drop (ignore transitions from 0 — entity just spawned or uninitialized)
|
|
if (settingsPanel_.damageFlashEnabled_ && lastPlayerHp_ > 0 && currentHp < lastPlayerHp_ && currentHp > 0)
|
|
damageFlashAlpha_ = 1.0f;
|
|
lastPlayerHp_ = currentHp;
|
|
|
|
// Fade out over ~0.5 seconds
|
|
if (damageFlashAlpha_ > 0.0f) {
|
|
damageFlashAlpha_ -= ImGui::GetIO().DeltaTime * 2.0f;
|
|
if (damageFlashAlpha_ < 0.0f) damageFlashAlpha_ = 0.0f;
|
|
drawScreenEdgeVignette(200, 0, 0,
|
|
static_cast<int>(damageFlashAlpha_ * 100.0f), 0.12f);
|
|
}
|
|
}
|
|
|
|
// Persistent low-health vignette — pulsing red edges when HP < 20%
|
|
{
|
|
auto playerEntity = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid());
|
|
bool isDead = gameHandler.isPlayerDead();
|
|
float hpPct = 1.0f;
|
|
if (!isDead && playerEntity &&
|
|
(playerEntity->getType() == game::ObjectType::PLAYER ||
|
|
playerEntity->getType() == game::ObjectType::UNIT)) {
|
|
auto unit = std::static_pointer_cast<game::Unit>(playerEntity);
|
|
if (unit->getMaxHealth() > 0)
|
|
hpPct = static_cast<float>(unit->getHealth()) / static_cast<float>(unit->getMaxHealth());
|
|
}
|
|
|
|
// Only show when alive and below 20% HP; intensity increases as HP drops
|
|
if (settingsPanel_.lowHealthVignetteEnabled_ && !isDead && hpPct < 0.20f && hpPct > 0.0f) {
|
|
// Base intensity from HP deficit (0 at 20%, 1 at 0%); pulse at ~1.5 Hz
|
|
float danger = (0.20f - hpPct) / 0.20f;
|
|
float pulse = 0.55f + 0.45f * std::sin(static_cast<float>(ImGui::GetTime()) * 9.4f);
|
|
int alpha = static_cast<int>(danger * pulse * 90.0f); // max ~90 alpha, subtle
|
|
drawScreenEdgeVignette(200, 0, 0, alpha, 0.15f);
|
|
}
|
|
}
|
|
|
|
// Level-up golden burst overlay
|
|
if (toastManager_.levelUpFlashAlpha > 0.0f) {
|
|
toastManager_.levelUpFlashAlpha -= ImGui::GetIO().DeltaTime * 1.0f; // fade over ~1 second
|
|
if (toastManager_.levelUpFlashAlpha < 0.0f) toastManager_.levelUpFlashAlpha = 0.0f;
|
|
|
|
const int alpha = static_cast<int>(toastManager_.levelUpFlashAlpha * 160.0f);
|
|
drawScreenEdgeVignette(255, 210, 50, alpha, 0.18f);
|
|
|
|
// "Level X!" text in the center during the first half of the animation
|
|
if (toastManager_.levelUpFlashAlpha > 0.5f && toastManager_.levelUpDisplayLevel > 0) {
|
|
ImDrawList* fg = ImGui::GetForegroundDrawList();
|
|
const float W = ImGui::GetIO().DisplaySize.x;
|
|
const float H = ImGui::GetIO().DisplaySize.y;
|
|
char lvlText[32];
|
|
snprintf(lvlText, sizeof(lvlText), "Level %u!", toastManager_.levelUpDisplayLevel);
|
|
ImVec2 ts = ImGui::CalcTextSize(lvlText);
|
|
float tx = (W - ts.x) * 0.5f;
|
|
float ty = H * 0.35f;
|
|
// Large shadow + bright gold text
|
|
fg->AddText(nullptr, 28.0f, ImVec2(tx + 2, ty + 2), IM_COL32(0, 0, 0, alpha), lvlText);
|
|
fg->AddText(nullptr, 28.0f, ImVec2(tx, ty), IM_COL32(255, 230, 80, alpha), lvlText);
|
|
}
|
|
}
|
|
|
|
// Restore previous alpha
|
|
ImGui::GetStyle().Alpha = prevAlpha;
|
|
}
|
|
|
|
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(kColorBrightGreen, "In World");
|
|
break;
|
|
case game::WorldState::AUTHENTICATED:
|
|
ImGui::TextColored(kColorYellow, "Authenticated");
|
|
break;
|
|
case game::WorldState::ENTERING_WORLD:
|
|
ImGui::TextColored(kColorYellow, "Entering World...");
|
|
break;
|
|
default:
|
|
ImGui::TextColored(kColorRed, "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);
|
|
char guidStr[24];
|
|
snprintf(guidStr, sizeof(guidStr), "0x%016llX", (unsigned long long)guid);
|
|
ImGui::Text("%s", guidStr);
|
|
|
|
// Type
|
|
ImGui::TableSetColumnIndex(1);
|
|
switch (entity->getType()) {
|
|
case game::ObjectType::PLAYER:
|
|
ImGui::TextColored(kColorBrightGreen, "Player");
|
|
break;
|
|
case game::ObjectType::UNIT:
|
|
ImGui::TextColored(kColorYellow, "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::processTargetInput(game::GameHandler& gameHandler) {
|
|
auto& io = ImGui::GetIO();
|
|
auto& input = core::Input::getInstance();
|
|
|
|
// If the user is typing (or about to focus chat this frame), do not allow
|
|
// A-Z or 1-0 shortcuts to fire.
|
|
if (!io.WantTextInput && !chatPanel_.isChatInputActive() && input.isKeyJustPressed(SDL_SCANCODE_SLASH)) {
|
|
chatPanel_.activateSlashInput();
|
|
}
|
|
if (!io.WantTextInput && !chatPanel_.isChatInputActive() &&
|
|
KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHAT, false)) {
|
|
chatPanel_.activateInput();
|
|
}
|
|
|
|
const bool textFocus = chatPanel_.isChatInputActive() || io.WantTextInput;
|
|
|
|
// Game hotkeys — gate on textFocus (chat/text-input active) rather than
|
|
// WantCaptureKeyboard so that toggle keys like M, C, I still work when an
|
|
// ImGui window (character panel, map, etc.) happens to have focus.
|
|
{
|
|
if (!textFocus && input.isKeyJustPressed(SDL_SCANCODE_TAB)) {
|
|
const auto& movement = gameHandler.getMovementInfo();
|
|
gameHandler.tabTarget(movement.x, movement.y, movement.z);
|
|
}
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_SETTINGS, true)) {
|
|
if (settingsPanel_.showSettingsWindow) {
|
|
settingsPanel_.showSettingsWindow = false;
|
|
} else if (windowManager_.showEscapeMenu) {
|
|
windowManager_.showEscapeMenu = false;
|
|
settingsPanel_.showEscapeSettingsNotice = false;
|
|
} else if (gameHandler.isCasting()) {
|
|
gameHandler.cancelCast();
|
|
} else if (gameHandler.isLootWindowOpen()) {
|
|
gameHandler.closeLoot();
|
|
} else if (gameHandler.isGossipWindowOpen()) {
|
|
gameHandler.closeGossip();
|
|
} else if (gameHandler.isVendorWindowOpen()) {
|
|
gameHandler.closeVendor();
|
|
} else if (gameHandler.isBarberShopOpen()) {
|
|
gameHandler.closeBarberShop();
|
|
} else if (gameHandler.isBankOpen()) {
|
|
gameHandler.closeBank();
|
|
} else if (gameHandler.isTrainerWindowOpen()) {
|
|
gameHandler.closeTrainer();
|
|
} else if (gameHandler.isMailboxOpen()) {
|
|
gameHandler.closeMailbox();
|
|
} else if (gameHandler.isAuctionHouseOpen()) {
|
|
gameHandler.closeAuctionHouse();
|
|
} else if (gameHandler.isQuestDetailsOpen()) {
|
|
gameHandler.declineQuest();
|
|
} else if (gameHandler.isQuestOfferRewardOpen()) {
|
|
gameHandler.closeQuestOfferReward();
|
|
} else if (gameHandler.isQuestRequestItemsOpen()) {
|
|
gameHandler.closeQuestRequestItems();
|
|
} else if (gameHandler.isTradeOpen()) {
|
|
gameHandler.cancelTrade();
|
|
} else if (socialPanel_.showWhoWindow_) {
|
|
socialPanel_.showWhoWindow_ = false;
|
|
} else if (combatUI_.showCombatLog_) {
|
|
combatUI_.showCombatLog_ = false;
|
|
} else if (socialPanel_.showSocialFrame_) {
|
|
socialPanel_.showSocialFrame_ = false;
|
|
} else if (talentScreen.isOpen()) {
|
|
talentScreen.setOpen(false);
|
|
} else if (spellbookScreen.isOpen()) {
|
|
spellbookScreen.setOpen(false);
|
|
} else if (questLogScreen.isOpen()) {
|
|
questLogScreen.setOpen(false);
|
|
} else if (inventoryScreen.isCharacterOpen()) {
|
|
inventoryScreen.toggleCharacter();
|
|
} else if (inventoryScreen.isOpen()) {
|
|
inventoryScreen.setOpen(false);
|
|
} else if (showWorldMap_) {
|
|
showWorldMap_ = false;
|
|
} else {
|
|
windowManager_.showEscapeMenu = true;
|
|
}
|
|
}
|
|
|
|
if (!textFocus) {
|
|
// Toggle character screen (C) and inventory/bags (I)
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHARACTER_SCREEN)) {
|
|
const bool wasOpen = inventoryScreen.isCharacterOpen();
|
|
inventoryScreen.toggleCharacter();
|
|
if (!wasOpen && gameHandler.isConnected()) {
|
|
gameHandler.requestPlayedTime();
|
|
}
|
|
}
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_INVENTORY)) {
|
|
inventoryScreen.toggle();
|
|
}
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_NAMEPLATES)) {
|
|
if (ImGui::GetIO().KeyShift)
|
|
settingsPanel_.showFriendlyNameplates_ = !settingsPanel_.showFriendlyNameplates_;
|
|
else
|
|
showNameplates_ = !showNameplates_;
|
|
}
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_WORLD_MAP)) {
|
|
showWorldMap_ = !showWorldMap_;
|
|
}
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_MINIMAP)) {
|
|
showMinimap_ = !showMinimap_;
|
|
}
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_RAID_FRAMES)) {
|
|
socialPanel_.showRaidFrames_ = !socialPanel_.showRaidFrames_;
|
|
}
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_ACHIEVEMENTS)) {
|
|
windowManager_.showAchievementWindow_ = !windowManager_.showAchievementWindow_;
|
|
}
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_SKILLS)) {
|
|
windowManager_.showSkillsWindow_ = !windowManager_.showSkillsWindow_;
|
|
}
|
|
|
|
// Toggle Titles window with H (hero/title screen — no conflicting keybinding)
|
|
if (input.isKeyJustPressed(SDL_SCANCODE_H)) {
|
|
windowManager_.showTitlesWindow_ = !windowManager_.showTitlesWindow_;
|
|
}
|
|
|
|
// Screenshot (PrintScreen key)
|
|
if (input.isKeyJustPressed(SDL_SCANCODE_PRINTSCREEN)) {
|
|
takeScreenshot(gameHandler);
|
|
}
|
|
|
|
// Action bar keys (1-9, 0, -, =)
|
|
static const SDL_Scancode actionBarKeys[] = {
|
|
SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_3, SDL_SCANCODE_4,
|
|
SDL_SCANCODE_5, SDL_SCANCODE_6, SDL_SCANCODE_7, SDL_SCANCODE_8,
|
|
SDL_SCANCODE_9, SDL_SCANCODE_0, SDL_SCANCODE_MINUS, SDL_SCANCODE_EQUALS
|
|
};
|
|
const bool shiftDown = input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT);
|
|
const bool ctrlDown = input.isKeyPressed(SDL_SCANCODE_LCTRL) || input.isKeyPressed(SDL_SCANCODE_RCTRL);
|
|
const auto& bar = gameHandler.getActionBar();
|
|
|
|
// Ctrl+1..Ctrl+8 → switch stance/form/presence (WoW default bindings).
|
|
// Only fires for classes that use a stance bar; same slot ordering as
|
|
// renderStanceBar: Warrior, DK, Druid, Rogue, Priest.
|
|
if (ctrlDown) {
|
|
static constexpr uint32_t warriorStances[] = { 2457, 71, 2458 };
|
|
static constexpr uint32_t dkPresences[] = { 48266, 48263, 48265 };
|
|
static constexpr uint32_t druidForms[] = { 5487, 9634, 768, 783, 1066, 24858, 33891, 33943, 40120 };
|
|
static constexpr uint32_t rogueForms[] = { 1784 };
|
|
static constexpr uint32_t priestForms[] = { 15473 };
|
|
const uint32_t* stArr = nullptr; int stCnt = 0;
|
|
switch (gameHandler.getPlayerClass()) {
|
|
case 1: stArr = warriorStances; stCnt = 3; break;
|
|
case 6: stArr = dkPresences; stCnt = 3; break;
|
|
case 11: stArr = druidForms; stCnt = 9; break;
|
|
case 4: stArr = rogueForms; stCnt = 1; break;
|
|
case 5: stArr = priestForms; stCnt = 1; break;
|
|
}
|
|
if (stArr) {
|
|
const auto& known = gameHandler.getKnownSpells();
|
|
// Build available list (same order as UI)
|
|
std::vector<uint32_t> avail;
|
|
avail.reserve(stCnt);
|
|
for (int i = 0; i < stCnt; ++i)
|
|
if (known.count(stArr[i])) avail.push_back(stArr[i]);
|
|
// Ctrl+1 = first stance, Ctrl+2 = second, …
|
|
for (int i = 0; i < static_cast<int>(avail.size()) && i < 8; ++i) {
|
|
if (input.isKeyJustPressed(actionBarKeys[i]))
|
|
gameHandler.castSpell(avail[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < game::GameHandler::SLOTS_PER_BAR; ++i) {
|
|
if (!ctrlDown && input.isKeyJustPressed(actionBarKeys[i])) {
|
|
int slotIdx = shiftDown ? (game::GameHandler::SLOTS_PER_BAR + i) : i;
|
|
if (bar[slotIdx].type == game::ActionBarSlot::SPELL && bar[slotIdx].isReady()) {
|
|
uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
|
|
gameHandler.castSpell(bar[slotIdx].id, target);
|
|
} else if (bar[slotIdx].type == game::ActionBarSlot::ITEM && bar[slotIdx].id != 0) {
|
|
gameHandler.useItemById(bar[slotIdx].id);
|
|
} else if (bar[slotIdx].type == game::ActionBarSlot::MACRO) {
|
|
chatPanel_.executeMacroText(gameHandler, gameHandler.getMacroText(bar[slotIdx].id));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// Cursor affordance: show hand cursor over interactable entities.
|
|
if (!io.WantCaptureMouse) {
|
|
auto* renderer = services_.renderer;
|
|
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
|
auto* window = services_.window;
|
|
if (camera && window) {
|
|
glm::vec2 mousePos = input.getMousePosition();
|
|
float screenW = static_cast<float>(window->getWidth());
|
|
float screenH = static_cast<float>(window->getHeight());
|
|
rendering::Ray ray = camera->screenToWorldRay(mousePos.x, mousePos.y, screenW, screenH);
|
|
float closestT = 1e30f;
|
|
bool hoverInteractable = false;
|
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
|
bool isGo = (entity->getType() == game::ObjectType::GAMEOBJECT);
|
|
bool isUnit = (entity->getType() == game::ObjectType::UNIT);
|
|
bool isPlayer = (entity->getType() == game::ObjectType::PLAYER);
|
|
if (!isGo && !isUnit && !isPlayer) continue;
|
|
if (guid == gameHandler.getPlayerGuid()) continue; // skip self
|
|
|
|
glm::vec3 hitCenter;
|
|
float hitRadius = 0.0f;
|
|
bool hasBounds = core::Application::getInstance().getRenderBoundsForGuid(guid, hitCenter, hitRadius);
|
|
if (!hasBounds) {
|
|
hitRadius = isGo ? 2.5f : 1.8f;
|
|
hitCenter = core::coords::canonicalToRender(glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
|
hitCenter.z += isGo ? 1.2f : 1.0f;
|
|
} else {
|
|
hitRadius = std::max(hitRadius * 1.1f, 0.8f);
|
|
}
|
|
|
|
float hitT;
|
|
if (raySphereIntersect(ray, hitCenter, hitRadius, hitT) && hitT < closestT) {
|
|
closestT = hitT;
|
|
hoverInteractable = true;
|
|
}
|
|
}
|
|
if (hoverInteractable) {
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Left-click targeting: only on mouse-up if the mouse didn't drag (camera rotate)
|
|
// Record press position on mouse-down
|
|
if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_LEFT) && !input.isMouseButtonPressed(SDL_BUTTON_RIGHT)) {
|
|
leftClickPressPos_ = input.getMousePosition();
|
|
leftClickWasPress_ = true;
|
|
}
|
|
|
|
// On mouse-up, check if it was a click (not a drag)
|
|
if (leftClickWasPress_ && input.isMouseButtonJustReleased(SDL_BUTTON_LEFT)) {
|
|
leftClickWasPress_ = false;
|
|
glm::vec2 releasePos = input.getMousePosition();
|
|
glm::vec2 dragDelta = releasePos - leftClickPressPos_;
|
|
float dragDistSq = glm::dot(dragDelta, dragDelta);
|
|
constexpr float CLICK_THRESHOLD = 5.0f; // pixels
|
|
|
|
if (dragDistSq < CLICK_THRESHOLD * CLICK_THRESHOLD) {
|
|
auto* renderer = services_.renderer;
|
|
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
|
auto* window = services_.window;
|
|
|
|
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;
|
|
float closestHostileUnitT = 1e30f;
|
|
uint64_t closestHostileUnitGuid = 0;
|
|
|
|
const uint64_t myGuid = gameHandler.getPlayerGuid();
|
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
|
auto t = entity->getType();
|
|
if (t != game::ObjectType::UNIT &&
|
|
t != game::ObjectType::PLAYER &&
|
|
t != game::ObjectType::GAMEOBJECT) continue;
|
|
if (guid == myGuid) continue; // 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;
|
|
}
|
|
} else if (t == game::ObjectType::GAMEOBJECT) {
|
|
hitRadius = 2.5f;
|
|
heightOffset = 1.2f;
|
|
}
|
|
hitCenter = core::coords::canonicalToRender(glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
|
hitCenter.z += heightOffset;
|
|
} else {
|
|
hitRadius = std::max(hitRadius * 1.1f, 0.6f);
|
|
}
|
|
|
|
float hitT;
|
|
if (raySphereIntersect(ray, hitCenter, hitRadius, hitT)) {
|
|
if (t == game::ObjectType::UNIT) {
|
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
|
bool hostileUnit = unit->isHostile() || gameHandler.isAggressiveTowardPlayer(guid);
|
|
if (hostileUnit && hitT < closestHostileUnitT) {
|
|
closestHostileUnitT = hitT;
|
|
closestHostileUnitGuid = guid;
|
|
}
|
|
}
|
|
if (hitT < closestT) {
|
|
closestT = hitT;
|
|
closestGuid = guid;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Prefer hostile monsters over nearby gameobjects/others when both are hittable.
|
|
if (closestHostileUnitGuid != 0) {
|
|
closestGuid = closestHostileUnitGuid;
|
|
}
|
|
|
|
if (closestGuid != 0) {
|
|
gameHandler.setTarget(closestGuid);
|
|
} else {
|
|
// Clicked empty space — deselect current target
|
|
gameHandler.clearTarget();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Right-click: select NPC (if needed) then interact / loot / auto-attack
|
|
// Suppress when left button is held (both-button run)
|
|
if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_RIGHT) && !input.isMouseButtonPressed(SDL_BUTTON_LEFT)) {
|
|
// If a gameobject is already targeted, prioritize interacting with that target
|
|
// instead of re-picking under cursor (which can hit nearby decorative GOs).
|
|
if (gameHandler.hasTarget()) {
|
|
auto target = gameHandler.getTarget();
|
|
if (target && target->getType() == game::ObjectType::GAMEOBJECT) {
|
|
LOG_DEBUG("[GO-DIAG] Right-click: re-interacting with targeted GO 0x",
|
|
std::hex, target->getGuid(), std::dec);
|
|
gameHandler.setTarget(target->getGuid());
|
|
gameHandler.interactWithGameObject(target->getGuid());
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If no target or right-clicking in world, try to pick one under cursor
|
|
{
|
|
auto* renderer = services_.renderer;
|
|
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
|
auto* window = services_.window;
|
|
if (camera && window) {
|
|
// If a quest objective gameobject is under the cursor, prefer it over
|
|
// hostile units so quest pickups (e.g. "Bundle of Wood") are reliable.
|
|
std::unordered_set<uint32_t> questObjectiveGoEntries;
|
|
{
|
|
const auto& ql = gameHandler.getQuestLog();
|
|
questObjectiveGoEntries.reserve(32);
|
|
for (const auto& q : ql) {
|
|
if (q.complete) continue;
|
|
for (const auto& obj : q.killObjectives) {
|
|
if (obj.npcOrGoId >= 0 || obj.required == 0) continue;
|
|
uint32_t entry = static_cast<uint32_t>(-obj.npcOrGoId);
|
|
uint32_t cur = 0;
|
|
auto it = q.killCounts.find(entry);
|
|
if (it != q.killCounts.end()) cur = it->second.first;
|
|
if (cur < obj.required) questObjectiveGoEntries.insert(entry);
|
|
}
|
|
}
|
|
}
|
|
|
|
glm::vec2 mousePos = input.getMousePosition();
|
|
float screenW = static_cast<float>(window->getWidth());
|
|
float screenH = static_cast<float>(window->getHeight());
|
|
rendering::Ray ray = camera->screenToWorldRay(mousePos.x, mousePos.y, screenW, screenH);
|
|
float closestT = 1e30f;
|
|
uint64_t closestGuid = 0;
|
|
game::ObjectType closestType = game::ObjectType::OBJECT;
|
|
float closestHostileUnitT = 1e30f;
|
|
uint64_t closestHostileUnitGuid = 0;
|
|
float closestQuestGoT = 1e30f;
|
|
uint64_t closestQuestGoGuid = 0;
|
|
float closestGoT = 1e30f;
|
|
uint64_t closestGoGuid = 0;
|
|
const uint64_t myGuid = gameHandler.getPlayerGuid();
|
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
|
auto t = entity->getType();
|
|
if (t != game::ObjectType::UNIT &&
|
|
t != game::ObjectType::PLAYER &&
|
|
t != game::ObjectType::GAMEOBJECT)
|
|
continue;
|
|
if (guid == myGuid) continue;
|
|
|
|
glm::vec3 hitCenter;
|
|
float hitRadius = 0.0f;
|
|
bool hasBounds = core::Application::getInstance().getRenderBoundsForGuid(guid, hitCenter, hitRadius);
|
|
if (!hasBounds) {
|
|
float heightOffset = 1.5f;
|
|
hitRadius = 1.5f;
|
|
if (t == game::ObjectType::UNIT) {
|
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
|
if (unit->getMaxHealth() > 0 && unit->getMaxHealth() < 100) {
|
|
hitRadius = 0.5f;
|
|
heightOffset = 0.3f;
|
|
}
|
|
} else if (t == game::ObjectType::GAMEOBJECT) {
|
|
hitRadius = 2.5f;
|
|
heightOffset = 1.2f;
|
|
}
|
|
hitCenter = core::coords::canonicalToRender(
|
|
glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
|
hitCenter.z += heightOffset;
|
|
// Log each unique GO's raypick position once
|
|
if (t == game::ObjectType::GAMEOBJECT) {
|
|
static std::unordered_set<uint64_t> goPickLog;
|
|
if (goPickLog.insert(guid).second) {
|
|
auto go = std::static_pointer_cast<game::GameObject>(entity);
|
|
LOG_DEBUG("[GO-DIAG] Raypick GO: guid=0x", std::hex, guid, std::dec,
|
|
" entry=", go->getEntry(), " name='", go->getName(),
|
|
"' pos=(", entity->getX(), ",", entity->getY(), ",", entity->getZ(),
|
|
") center=(", hitCenter.x, ",", hitCenter.y, ",", hitCenter.z,
|
|
") r=", hitRadius);
|
|
}
|
|
}
|
|
} else {
|
|
hitRadius = std::max(hitRadius * 1.1f, 0.6f);
|
|
}
|
|
|
|
float hitT;
|
|
if (raySphereIntersect(ray, hitCenter, hitRadius, hitT)) {
|
|
if (t == game::ObjectType::UNIT) {
|
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
|
bool hostileUnit = unit->isHostile() || gameHandler.isAggressiveTowardPlayer(guid);
|
|
if (hostileUnit && hitT < closestHostileUnitT) {
|
|
closestHostileUnitT = hitT;
|
|
closestHostileUnitGuid = guid;
|
|
}
|
|
}
|
|
if (t == game::ObjectType::GAMEOBJECT) {
|
|
if (hitT < closestGoT) {
|
|
closestGoT = hitT;
|
|
closestGoGuid = guid;
|
|
}
|
|
if (!questObjectiveGoEntries.empty()) {
|
|
auto go = std::static_pointer_cast<game::GameObject>(entity);
|
|
if (questObjectiveGoEntries.count(go->getEntry())) {
|
|
if (hitT < closestQuestGoT) {
|
|
closestQuestGoT = hitT;
|
|
closestQuestGoGuid = guid;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (hitT < closestT) {
|
|
closestT = hitT;
|
|
closestGuid = guid;
|
|
closestType = t;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Priority: quest GO > closer of (GO, hostile unit) > closest anything.
|
|
if (closestQuestGoGuid != 0) {
|
|
closestGuid = closestQuestGoGuid;
|
|
closestType = game::ObjectType::GAMEOBJECT;
|
|
} else if (closestGoGuid != 0 && closestHostileUnitGuid != 0) {
|
|
// Both a GO and hostile unit were hit — prefer whichever is closer.
|
|
if (closestGoT <= closestHostileUnitT) {
|
|
closestGuid = closestGoGuid;
|
|
closestType = game::ObjectType::GAMEOBJECT;
|
|
} else {
|
|
closestGuid = closestHostileUnitGuid;
|
|
closestType = game::ObjectType::UNIT;
|
|
}
|
|
} else if (closestGoGuid != 0) {
|
|
closestGuid = closestGoGuid;
|
|
closestType = game::ObjectType::GAMEOBJECT;
|
|
} else if (closestHostileUnitGuid != 0) {
|
|
closestGuid = closestHostileUnitGuid;
|
|
closestType = game::ObjectType::UNIT;
|
|
}
|
|
|
|
if (closestGuid != 0) {
|
|
if (closestType == game::ObjectType::GAMEOBJECT) {
|
|
LOG_DEBUG("[GO-DIAG] Right-click: raypick hit GO 0x",
|
|
std::hex, closestGuid, std::dec);
|
|
gameHandler.setTarget(closestGuid);
|
|
gameHandler.interactWithGameObject(closestGuid);
|
|
return;
|
|
}
|
|
gameHandler.setTarget(closestGuid);
|
|
}
|
|
}
|
|
}
|
|
if (gameHandler.hasTarget()) {
|
|
auto target = gameHandler.getTarget();
|
|
if (target) {
|
|
if (target->getType() == game::ObjectType::UNIT) {
|
|
// Check if unit is dead (health == 0) → loot, otherwise interact/attack
|
|
auto unit = std::static_pointer_cast<game::Unit>(target);
|
|
if (unit->getHealth() == 0 && unit->getMaxHealth() > 0) {
|
|
gameHandler.lootTarget(target->getGuid());
|
|
} else {
|
|
// Interact with service NPCs; otherwise treat non-interactable living units
|
|
// as attackable fallback (covers bad faction-template classification).
|
|
auto isSpiritNpc = [&]() -> bool {
|
|
constexpr uint32_t NPC_FLAG_SPIRIT_GUIDE = 0x00004000;
|
|
constexpr uint32_t NPC_FLAG_SPIRIT_HEALER = 0x00008000;
|
|
if (unit->getNpcFlags() & (NPC_FLAG_SPIRIT_GUIDE | NPC_FLAG_SPIRIT_HEALER)) {
|
|
return true;
|
|
}
|
|
std::string name = unit->getName();
|
|
std::transform(name.begin(), name.end(), name.begin(),
|
|
[](unsigned char c){ return static_cast<char>(std::tolower(c)); });
|
|
return (name.find("spirit healer") != std::string::npos) ||
|
|
(name.find("spirit guide") != std::string::npos);
|
|
};
|
|
bool allowSpiritInteract = (gameHandler.isPlayerDead() || gameHandler.isPlayerGhost()) && isSpiritNpc();
|
|
bool canInteractNpc = unit->isInteractable() || allowSpiritInteract;
|
|
bool shouldAttackByFallback = !canInteractNpc;
|
|
if (!unit->isHostile() && canInteractNpc) {
|
|
gameHandler.interactWithNpc(target->getGuid());
|
|
} else if (unit->isHostile() || shouldAttackByFallback) {
|
|
gameHandler.startAutoAttack(target->getGuid());
|
|
}
|
|
}
|
|
} else if (target->getType() == game::ObjectType::GAMEOBJECT) {
|
|
gameHandler.interactWithGameObject(target->getGuid());
|
|
} else if (target->getType() == game::ObjectType::PLAYER) {
|
|
// Right-click another player could start attack in PvP context
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
}} // namespace wowee::ui
|