mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-05 00:33:51 +00:00
refactor(chat): extract ItemTooltipRenderer, slim render(), consolidate utils
- Extract renderItemTooltip() (510 LOC) from ChatMarkupRenderer into dedicated ItemTooltipRenderer class; chat_markup_renderer.cpp 766→192 LOC - Extract formatChatMessage(), detectChannelPrefix(), inputTextCallback() from render(); render() 711→376 LOC - Consolidate replaceGenderPlaceholders() from 3 copies into chat_utils::replaceGenderPlaceholders(); remove 118 LOC duplicate from quest_log_screen.cpp, update 8 call sites in window_manager.cpp - Delete chat_panel_commands.cpp (359 LOC) — absorb sendChatMessage, executeMacroText, PortBot helpers into chat_panel.cpp; move evaluateMacroConditionals to macro_eval_convenience.cpp - Delete chat_panel_utils.cpp (229 LOC) — absorb small utilities into chat_panel.cpp - Replace 3 forward declarations of evaluateMacroConditionals with #include "ui/chat/macro_evaluator.hpp" Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
This commit is contained in:
parent
42f1bb98ea
commit
ada019e0d4
17 changed files with 1302 additions and 1463 deletions
|
|
@ -685,17 +685,18 @@ set(WOWEE_SOURCES
|
|||
src/ui/game_screen_hud.cpp
|
||||
src/ui/game_screen_minimap.cpp
|
||||
src/ui/chat_panel.cpp
|
||||
src/ui/chat_panel_commands.cpp
|
||||
src/ui/chat_panel_utils.cpp
|
||||
src/ui/chat/chat_settings.cpp
|
||||
src/ui/chat/chat_input.cpp
|
||||
src/ui/chat/chat_utils.cpp
|
||||
src/ui/chat/chat_tab_manager.cpp
|
||||
src/ui/chat/chat_bubble_manager.cpp
|
||||
src/ui/chat/chat_markup_parser.cpp
|
||||
src/ui/chat/chat_markup_renderer.cpp
|
||||
src/ui/chat/item_tooltip_renderer.cpp
|
||||
src/ui/chat/chat_command_registry.cpp
|
||||
src/ui/chat/chat_tab_completer.cpp
|
||||
src/ui/chat/macro_evaluator.cpp
|
||||
src/ui/chat/macro_eval_convenience.cpp
|
||||
src/ui/chat/game_state_adapter.cpp
|
||||
src/ui/chat/input_modifier_adapter.cpp
|
||||
src/ui/chat/commands/system_commands.cpp
|
||||
|
|
|
|||
|
|
@ -47,15 +47,6 @@ public:
|
|||
void render(const std::vector<ChatSegment>& segments,
|
||||
const ImVec4& baseColor,
|
||||
const MarkupRenderContext& ctx) const;
|
||||
|
||||
/**
|
||||
* Render a full item tooltip for the given item entry.
|
||||
* Extracted from the renderItemLinkTooltip inline lambda.
|
||||
*/
|
||||
static void renderItemTooltip(uint32_t itemEntry,
|
||||
game::GameHandler& gameHandler,
|
||||
InventoryScreen& inventoryScreen,
|
||||
pipeline::AssetManager* assetMgr);
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
#pragma once
|
||||
|
||||
#include "game/world_packets.hpp"
|
||||
#include "game/entity.hpp"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace wowee {
|
||||
|
||||
// Forward declaration
|
||||
namespace game { class GameHandler; }
|
||||
// Forward declarations
|
||||
namespace game {
|
||||
class GameHandler;
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
namespace chat_utils {
|
||||
|
|
@ -38,6 +42,17 @@ inline std::string toLower(std::string s) {
|
|||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace $g/$G gender, $n/$N name, $c/$C class, $r/$R race,
|
||||
* $p/$o/$s/$S pronoun, $b/$B linebreak, and |n linebreak placeholders.
|
||||
* Extracted from ChatPanel::replaceGenderPlaceholders (Phase 6.6).
|
||||
*/
|
||||
std::string replaceGenderPlaceholders(const std::string& text,
|
||||
game::GameHandler& gameHandler);
|
||||
|
||||
/** Get display name for any entity (Player/Unit/GameObject). */
|
||||
std::string getEntityDisplayName(const std::shared_ptr<game::Entity>& entity);
|
||||
|
||||
} // namespace chat_utils
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
|
|
|
|||
28
include/ui/chat/item_tooltip_renderer.hpp
Normal file
28
include/ui/chat/item_tooltip_renderer.hpp
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace wowee {
|
||||
namespace game { class GameHandler; }
|
||||
namespace pipeline { class AssetManager; }
|
||||
namespace ui {
|
||||
|
||||
class InventoryScreen;
|
||||
|
||||
/**
|
||||
* Renders a full WoW-style item tooltip via ImGui.
|
||||
*
|
||||
* Extracted from ChatMarkupRenderer::renderItemTooltip (Phase 6.7).
|
||||
* Handles: item name/quality color, armor/DPS, stats, sockets, set bonuses,
|
||||
* durability, sell price, required level, comparison tooltip.
|
||||
*/
|
||||
class ItemTooltipRenderer {
|
||||
public:
|
||||
static void render(uint32_t itemEntry,
|
||||
game::GameHandler& gameHandler,
|
||||
InventoryScreen& inventoryScreen,
|
||||
pipeline::AssetManager* assetMgr);
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
#include <string>
|
||||
|
||||
namespace wowee {
|
||||
namespace game { class GameHandler; }
|
||||
namespace ui {
|
||||
|
||||
class IGameState;
|
||||
|
|
@ -46,5 +47,11 @@ private:
|
|||
IModifierState& modState_;
|
||||
};
|
||||
|
||||
// Convenience free function — thin wrapper over MacroEvaluator.
|
||||
// Used by command modules (combat_commands, system_commands, target_commands).
|
||||
std::string evaluateMacroConditionals(const std::string& rawArg,
|
||||
game::GameHandler& gameHandler,
|
||||
uint64_t& targetOverride);
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -125,9 +125,6 @@ public:
|
|||
// UIServices injection (Phase B singleton breaking)
|
||||
void setServices(const UIServices& services) { services_ = services; }
|
||||
|
||||
/** Replace $g/$G and $n/$N gender/name placeholders in quest/chat text. */
|
||||
std::string replaceGenderPlaceholders(const std::string& text, game::GameHandler& gameHandler);
|
||||
|
||||
// ---- Accessors for command system (Phase 3) ----
|
||||
char* getChatInputBuffer() { return chatInputBuffer_; }
|
||||
size_t getChatInputBufferSize() const { return sizeof(chatInputBuffer_); }
|
||||
|
|
@ -203,7 +200,8 @@ private:
|
|||
|
||||
// ---- Helpers ----
|
||||
void sendChatMessage(game::GameHandler& gameHandler);
|
||||
// getChatTypeName / getChatTypeColor now static in ChatTabManager
|
||||
static int inputTextCallback(ImGuiInputTextCallbackData* data);
|
||||
void detectChannelPrefix(game::GameHandler& gameHandler);
|
||||
|
||||
// Cached game handler for input callback (set each frame in render)
|
||||
game::GameHandler* cachedGameHandler_ = nullptr;
|
||||
|
|
|
|||
|
|
@ -1,531 +1,19 @@
|
|||
// ChatMarkupRenderer — render parsed ChatSegments via ImGui.
|
||||
// Moved from ChatPanel::render() inline lambdas (Phase 2.2).
|
||||
// Item tooltip rendering extracted to ItemTooltipRenderer (Phase 6.7).
|
||||
#include "ui/chat/chat_markup_renderer.hpp"
|
||||
#include "ui/chat/item_tooltip_renderer.hpp"
|
||||
#include "ui/ui_colors.hpp"
|
||||
#include "ui/inventory_screen.hpp"
|
||||
#include "ui/spellbook_screen.hpp"
|
||||
#include "ui/quest_log_screen.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "pipeline/dbc_layout.hpp"
|
||||
#include <imgui.h>
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
#include <cstdio>
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
// ---- renderItemTooltip (moved from renderItemLinkTooltip lambda) ----
|
||||
|
||||
void ChatMarkupRenderer::renderItemTooltip(
|
||||
uint32_t itemEntry,
|
||||
game::GameHandler& gameHandler,
|
||||
InventoryScreen& inventoryScreen,
|
||||
pipeline::AssetManager* assetMgr)
|
||||
{
|
||||
const auto* info = gameHandler.getItemInfo(itemEntry);
|
||||
if (!info || !info->valid) return;
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
ImGui::BeginTooltip();
|
||||
// Quality color for name
|
||||
auto qColor = ui::getQualityColor(static_cast<game::ItemQuality>(info->quality));
|
||||
ImGui::TextColored(qColor, "%s", info->name.c_str());
|
||||
|
||||
// Heroic indicator (green, matches WoW tooltip style)
|
||||
constexpr uint32_t kFlagHeroic = 0x8;
|
||||
constexpr uint32_t kFlagUniqueEquipped = 0x1000000;
|
||||
if (info->itemFlags & kFlagHeroic)
|
||||
ImGui::TextColored(ImVec4(0.0f, 0.8f, 0.0f, 1.0f), "Heroic");
|
||||
|
||||
// Bind type (appears right under name in WoW)
|
||||
switch (info->bindType) {
|
||||
case 1: ImGui::TextDisabled("Binds when picked up"); break;
|
||||
case 2: ImGui::TextDisabled("Binds when equipped"); break;
|
||||
case 3: ImGui::TextDisabled("Binds when used"); break;
|
||||
case 4: ImGui::TextDisabled("Quest Item"); break;
|
||||
}
|
||||
// Unique / Unique-Equipped
|
||||
if (info->maxCount == 1)
|
||||
ImGui::TextColored(ui::colors::kTooltipGold, "Unique");
|
||||
else if (info->itemFlags & kFlagUniqueEquipped)
|
||||
ImGui::TextColored(ui::colors::kTooltipGold, "Unique-Equipped");
|
||||
|
||||
// Slot type
|
||||
if (info->inventoryType > 0) {
|
||||
const char* slotName = ui::getInventorySlotName(info->inventoryType);
|
||||
if (slotName[0]) {
|
||||
if (!info->subclassName.empty())
|
||||
ImGui::TextColored(ui::colors::kLightGray, "%s %s", slotName, info->subclassName.c_str());
|
||||
else
|
||||
ImGui::TextColored(ui::colors::kLightGray, "%s", slotName);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
};
|
||||
const bool isWeapon = isWeaponInventoryType(info->inventoryType);
|
||||
|
||||
// Item level (after slot/subclass)
|
||||
if (info->itemLevel > 0)
|
||||
ImGui::TextDisabled("Item Level %u", info->itemLevel);
|
||||
|
||||
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;
|
||||
char dmgBuf[64], spdBuf[32];
|
||||
std::snprintf(dmgBuf, sizeof(dmgBuf), "%d - %d Damage",
|
||||
static_cast<int>(info->damageMin), static_cast<int>(info->damageMax));
|
||||
std::snprintf(spdBuf, sizeof(spdBuf), "Speed %.2f", speed);
|
||||
float spdW = ImGui::CalcTextSize(spdBuf).x;
|
||||
ImGui::Text("%s", dmgBuf);
|
||||
ImGui::SameLine(ImGui::GetWindowWidth() - spdW - 16.0f);
|
||||
ImGui::Text("%s", spdBuf);
|
||||
ImGui::TextDisabled("(%.1f damage per second)", dps);
|
||||
}
|
||||
ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f);
|
||||
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;
|
||||
};
|
||||
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());
|
||||
}
|
||||
if (info->armor > 0) {
|
||||
ImGui::Text("%d Armor", info->armor);
|
||||
}
|
||||
// Elemental resistances
|
||||
{
|
||||
const int32_t resVals[6] = {
|
||||
info->holyRes, info->fireRes, info->natureRes,
|
||||
info->frostRes, info->shadowRes, info->arcaneRes
|
||||
};
|
||||
static constexpr const char* resLabels[6] = {
|
||||
"Holy Resistance", "Fire Resistance", "Nature Resistance",
|
||||
"Frost Resistance", "Shadow Resistance", "Arcane Resistance"
|
||||
};
|
||||
for (int ri = 0; ri < 6; ++ri)
|
||||
if (resVals[ri] > 0) ImGui::Text("+%d %s", resVals[ri], resLabels[ri]);
|
||||
}
|
||||
// Extra stats (hit/crit/haste/sp/ap/expertise/resilience/etc.)
|
||||
if (!info->extraStats.empty()) {
|
||||
auto statName = [](uint32_t t) -> const char* {
|
||||
switch (t) {
|
||||
case 12: return "Defense Rating";
|
||||
case 13: return "Dodge Rating";
|
||||
case 14: return "Parry Rating";
|
||||
case 15: return "Block Rating";
|
||||
case 16: case 17: case 18: case 31: return "Hit Rating";
|
||||
case 19: case 20: case 21: case 32: return "Critical Strike Rating";
|
||||
case 28: case 29: case 30: case 35: return "Haste Rating";
|
||||
case 34: return "Resilience Rating";
|
||||
case 36: return "Expertise Rating";
|
||||
case 37: return "Attack Power";
|
||||
case 38: return "Ranged Attack Power";
|
||||
case 45: return "Spell Power";
|
||||
case 46: return "Healing Power";
|
||||
case 47: return "Spell Damage";
|
||||
case 49: return "Mana per 5 sec.";
|
||||
case 43: return "Spell Penetration";
|
||||
case 44: return "Block Value";
|
||||
default: return nullptr;
|
||||
}
|
||||
};
|
||||
for (const auto& es : info->extraStats) {
|
||||
const char* nm = statName(es.statType);
|
||||
if (nm && es.statValue > 0)
|
||||
ImGui::TextColored(green, "+%d %s", es.statValue, nm);
|
||||
}
|
||||
}
|
||||
// Gem sockets
|
||||
{
|
||||
const auto& kSocketTypes = ui::kSocketTypes;
|
||||
bool hasSocket = false;
|
||||
for (int s = 0; s < 3; ++s) {
|
||||
if (info->socketColor[s] == 0) continue;
|
||||
if (!hasSocket) { ImGui::Spacing(); hasSocket = true; }
|
||||
for (const auto& st : kSocketTypes) {
|
||||
if (info->socketColor[s] & st.mask) {
|
||||
ImGui::TextColored(st.col, "%s", st.label);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasSocket && info->socketBonus != 0) {
|
||||
static std::unordered_map<uint32_t, std::string> s_enchantNames;
|
||||
static bool s_enchantNamesLoaded = false;
|
||||
if (!s_enchantNamesLoaded && assetMgr) {
|
||||
s_enchantNamesLoaded = true;
|
||||
auto dbc = assetMgr->loadDBC("SpellItemEnchantment.dbc");
|
||||
if (dbc && dbc->isLoaded()) {
|
||||
const auto* lay = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("SpellItemEnchantment") : nullptr;
|
||||
uint32_t nameField = lay ? lay->field("Name") : 8u;
|
||||
if (nameField == 0xFFFFFFFF) nameField = 8;
|
||||
uint32_t fc = dbc->getFieldCount();
|
||||
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
||||
uint32_t eid = dbc->getUInt32(r, 0);
|
||||
if (eid == 0 || nameField >= fc) continue;
|
||||
std::string ename = dbc->getString(r, nameField);
|
||||
if (!ename.empty()) s_enchantNames[eid] = std::move(ename);
|
||||
}
|
||||
}
|
||||
}
|
||||
auto enchIt = s_enchantNames.find(info->socketBonus);
|
||||
if (enchIt != s_enchantNames.end())
|
||||
ImGui::TextColored(colors::kSocketGreen, "Socket Bonus: %s", enchIt->second.c_str());
|
||||
else
|
||||
ImGui::TextColored(colors::kSocketGreen, "Socket Bonus: (id %u)", info->socketBonus);
|
||||
}
|
||||
}
|
||||
// Item set membership
|
||||
if (info->itemSetId != 0) {
|
||||
struct SetEntry {
|
||||
std::string name;
|
||||
std::array<uint32_t, 10> itemIds{};
|
||||
std::array<uint32_t, 10> spellIds{};
|
||||
std::array<uint32_t, 10> thresholds{};
|
||||
};
|
||||
static std::unordered_map<uint32_t, SetEntry> s_setData;
|
||||
static bool s_setDataLoaded = false;
|
||||
if (!s_setDataLoaded && assetMgr) {
|
||||
s_setDataLoaded = true;
|
||||
auto dbc = assetMgr->loadDBC("ItemSet.dbc");
|
||||
if (dbc && dbc->isLoaded()) {
|
||||
const auto* layout = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("ItemSet") : nullptr;
|
||||
auto lf = [&](const char* k, uint32_t def) -> uint32_t {
|
||||
return layout ? (*layout)[k] : def;
|
||||
};
|
||||
uint32_t idF = lf("ID", 0), nameF = lf("Name", 1);
|
||||
const auto& itemKeys = ui::kItemSetItemKeys;
|
||||
const auto& spellKeys = ui::kItemSetSpellKeys;
|
||||
const auto& thrKeys = ui::kItemSetThresholdKeys;
|
||||
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
||||
uint32_t id = dbc->getUInt32(r, idF);
|
||||
if (!id) continue;
|
||||
SetEntry e;
|
||||
e.name = dbc->getString(r, nameF);
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
e.itemIds[i] = dbc->getUInt32(r, layout ? (*layout)[itemKeys[i]] : uint32_t(18 + i));
|
||||
e.spellIds[i] = dbc->getUInt32(r, layout ? (*layout)[spellKeys[i]] : uint32_t(28 + i));
|
||||
e.thresholds[i] = dbc->getUInt32(r, layout ? (*layout)[thrKeys[i]] : uint32_t(38 + i));
|
||||
}
|
||||
s_setData[id] = std::move(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::Spacing();
|
||||
const auto& inv = gameHandler.getInventory();
|
||||
auto setIt = s_setData.find(info->itemSetId);
|
||||
if (setIt != s_setData.end()) {
|
||||
const SetEntry& se = setIt->second;
|
||||
int equipped = 0, total = 0;
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
if (se.itemIds[i] == 0) continue;
|
||||
++total;
|
||||
for (int sl = 0; sl < game::Inventory::NUM_EQUIP_SLOTS; sl++) {
|
||||
const auto& eq = inv.getEquipSlot(static_cast<game::EquipSlot>(sl));
|
||||
if (!eq.empty() && eq.item.itemId == se.itemIds[i]) { ++equipped; break; }
|
||||
}
|
||||
}
|
||||
if (total > 0)
|
||||
ImGui::TextColored(ui::colors::kTooltipGold,
|
||||
"%s (%d/%d)", se.name.empty() ? "Set" : se.name.c_str(), equipped, total);
|
||||
else if (!se.name.empty())
|
||||
ImGui::TextColored(ui::colors::kTooltipGold, "%s", se.name.c_str());
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
if (se.spellIds[i] == 0 || se.thresholds[i] == 0) continue;
|
||||
const std::string& bname = gameHandler.getSpellName(se.spellIds[i]);
|
||||
bool active = (equipped >= static_cast<int>(se.thresholds[i]));
|
||||
ImVec4 col = active ? colors::kActiveGreen : colors::kInactiveGray;
|
||||
if (!bname.empty())
|
||||
ImGui::TextColored(col, "(%u) %s", se.thresholds[i], bname.c_str());
|
||||
else
|
||||
ImGui::TextColored(col, "(%u) Set Bonus", se.thresholds[i]);
|
||||
}
|
||||
} else {
|
||||
ImGui::TextColored(ui::colors::kTooltipGold, "Set (id %u)", info->itemSetId);
|
||||
}
|
||||
}
|
||||
// Item spell effects (Use / Equip / Chance on Hit / Teaches)
|
||||
for (const auto& sp : info->spells) {
|
||||
if (sp.spellId == 0) continue;
|
||||
const char* triggerLabel = nullptr;
|
||||
switch (sp.spellTrigger) {
|
||||
case 0: triggerLabel = "Use"; break;
|
||||
case 1: triggerLabel = "Equip"; break;
|
||||
case 2: triggerLabel = "Chance on Hit"; break;
|
||||
case 5: triggerLabel = "Teaches"; break;
|
||||
}
|
||||
if (!triggerLabel) continue;
|
||||
const std::string& spDesc = gameHandler.getSpellDescription(sp.spellId);
|
||||
const std::string& spText = !spDesc.empty() ? spDesc
|
||||
: gameHandler.getSpellName(sp.spellId);
|
||||
if (!spText.empty()) {
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 300.0f);
|
||||
ImGui::TextColored(colors::kCyan,
|
||||
"%s: %s", triggerLabel, spText.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
}
|
||||
// Required level
|
||||
if (info->requiredLevel > 1)
|
||||
ImGui::TextDisabled("Requires Level %u", info->requiredLevel);
|
||||
// Required skill
|
||||
if (info->requiredSkill != 0 && info->requiredSkillRank > 0) {
|
||||
static std::unordered_map<uint32_t, std::string> s_skillNames;
|
||||
static bool s_skillNamesLoaded = false;
|
||||
if (!s_skillNamesLoaded && assetMgr) {
|
||||
s_skillNamesLoaded = true;
|
||||
auto dbc = assetMgr->loadDBC("SkillLine.dbc");
|
||||
if (dbc && dbc->isLoaded()) {
|
||||
const auto* layout = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("SkillLine") : nullptr;
|
||||
uint32_t idF = layout ? (*layout)["ID"] : 0u;
|
||||
uint32_t nameF = layout ? (*layout)["Name"] : 2u;
|
||||
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
||||
uint32_t sid = dbc->getUInt32(r, idF);
|
||||
if (!sid) continue;
|
||||
std::string sname = dbc->getString(r, nameF);
|
||||
if (!sname.empty()) s_skillNames[sid] = std::move(sname);
|
||||
}
|
||||
}
|
||||
}
|
||||
uint32_t playerSkillVal = 0;
|
||||
const auto& skills = gameHandler.getPlayerSkills();
|
||||
auto skPit = skills.find(info->requiredSkill);
|
||||
if (skPit != skills.end()) playerSkillVal = skPit->second.effectiveValue();
|
||||
bool meetsSkill = (playerSkillVal == 0 || playerSkillVal >= info->requiredSkillRank);
|
||||
ImVec4 skColor = meetsSkill ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : colors::kPaleRed;
|
||||
auto skIt = s_skillNames.find(info->requiredSkill);
|
||||
if (skIt != s_skillNames.end())
|
||||
ImGui::TextColored(skColor, "Requires %s (%u)", skIt->second.c_str(), info->requiredSkillRank);
|
||||
else
|
||||
ImGui::TextColored(skColor, "Requires Skill %u (%u)", info->requiredSkill, info->requiredSkillRank);
|
||||
}
|
||||
// Required reputation
|
||||
if (info->requiredReputationFaction != 0 && info->requiredReputationRank > 0) {
|
||||
static std::unordered_map<uint32_t, std::string> s_factionNames;
|
||||
static bool s_factionNamesLoaded = false;
|
||||
if (!s_factionNamesLoaded && assetMgr) {
|
||||
s_factionNamesLoaded = true;
|
||||
auto dbc = assetMgr->loadDBC("Faction.dbc");
|
||||
if (dbc && dbc->isLoaded()) {
|
||||
const auto* layout = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("Faction") : nullptr;
|
||||
uint32_t idF = layout ? (*layout)["ID"] : 0u;
|
||||
uint32_t nameF = layout ? (*layout)["Name"] : 20u;
|
||||
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
||||
uint32_t fid = dbc->getUInt32(r, idF);
|
||||
if (!fid) continue;
|
||||
std::string fname = dbc->getString(r, nameF);
|
||||
if (!fname.empty()) s_factionNames[fid] = std::move(fname);
|
||||
}
|
||||
}
|
||||
}
|
||||
static constexpr const char* kRepRankNames[] = {
|
||||
"Hated", "Hostile", "Unfriendly", "Neutral",
|
||||
"Friendly", "Honored", "Revered", "Exalted"
|
||||
};
|
||||
const char* rankName = (info->requiredReputationRank < 8)
|
||||
? kRepRankNames[info->requiredReputationRank] : "Unknown";
|
||||
auto fIt = s_factionNames.find(info->requiredReputationFaction);
|
||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.75f), "Requires %s with %s",
|
||||
rankName,
|
||||
fIt != s_factionNames.end() ? fIt->second.c_str() : "Unknown Faction");
|
||||
}
|
||||
// Class restriction
|
||||
if (info->allowableClass != 0) {
|
||||
const auto& kClasses = ui::kClassMasks;
|
||||
int matchCount = 0;
|
||||
for (const auto& kc : kClasses)
|
||||
if (info->allowableClass & kc.mask) ++matchCount;
|
||||
if (matchCount > 0 && matchCount < 10) {
|
||||
char classBuf[128] = "Classes: ";
|
||||
bool first = true;
|
||||
for (const auto& kc : kClasses) {
|
||||
if (!(info->allowableClass & kc.mask)) continue;
|
||||
if (!first) strncat(classBuf, ", ", sizeof(classBuf) - strlen(classBuf) - 1);
|
||||
strncat(classBuf, kc.name, sizeof(classBuf) - strlen(classBuf) - 1);
|
||||
first = false;
|
||||
}
|
||||
uint8_t pc = gameHandler.getPlayerClass();
|
||||
uint32_t pmask = (pc > 0 && pc <= 10) ? (1u << (pc - 1)) : 0u;
|
||||
bool playerAllowed = (pmask == 0 || (info->allowableClass & pmask));
|
||||
ImVec4 clColor = playerAllowed ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : colors::kPaleRed;
|
||||
ImGui::TextColored(clColor, "%s", classBuf);
|
||||
}
|
||||
}
|
||||
// Race restriction
|
||||
if (info->allowableRace != 0) {
|
||||
const auto& kRaces = ui::kRaceMasks;
|
||||
constexpr uint32_t kAllPlayable = 1|2|4|8|16|32|64|128|512|1024;
|
||||
if ((info->allowableRace & kAllPlayable) != kAllPlayable) {
|
||||
int matchCount = 0;
|
||||
for (const auto& kr : kRaces)
|
||||
if (info->allowableRace & kr.mask) ++matchCount;
|
||||
if (matchCount > 0) {
|
||||
char raceBuf[160] = "Races: ";
|
||||
bool first = true;
|
||||
for (const auto& kr : kRaces) {
|
||||
if (!(info->allowableRace & kr.mask)) continue;
|
||||
if (!first) strncat(raceBuf, ", ", sizeof(raceBuf) - strlen(raceBuf) - 1);
|
||||
strncat(raceBuf, kr.name, sizeof(raceBuf) - strlen(raceBuf) - 1);
|
||||
first = false;
|
||||
}
|
||||
uint8_t pr = gameHandler.getPlayerRace();
|
||||
uint32_t pmask = (pr > 0 && pr <= 11) ? (1u << (pr - 1)) : 0u;
|
||||
bool playerAllowed = (pmask == 0 || (info->allowableRace & pmask));
|
||||
ImVec4 rColor = playerAllowed ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : colors::kPaleRed;
|
||||
ImGui::TextColored(rColor, "%s", raceBuf);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Flavor text
|
||||
if (!info->description.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::PushTextWrapPos(300.0f);
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 0.85f), "\"%s\"", info->description.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
if (info->sellPrice > 0) {
|
||||
ImGui::TextDisabled("Sell:"); ImGui::SameLine(0, 4);
|
||||
renderCoinsFromCopper(info->sellPrice);
|
||||
}
|
||||
|
||||
if (ImGui::GetIO().KeyShift && info->inventoryType > 0) {
|
||||
if (const auto* eq = findComparableEquipped(static_cast<uint8_t>(info->inventoryType))) {
|
||||
ImGui::Separator();
|
||||
ImGui::TextDisabled("Equipped:");
|
||||
VkDescriptorSet eqIcon = inventoryScreen.getItemIcon(eq->item.displayInfoId);
|
||||
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());
|
||||
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;
|
||||
char eqDmg[64], eqSpd[32];
|
||||
std::snprintf(eqDmg, sizeof(eqDmg), "%d - %d Damage",
|
||||
static_cast<int>(eq->item.damageMin), static_cast<int>(eq->item.damageMax));
|
||||
std::snprintf(eqSpd, sizeof(eqSpd), "Speed %.2f", speed);
|
||||
float eqSpdW = ImGui::CalcTextSize(eqSpd).x;
|
||||
ImGui::Text("%s", eqDmg);
|
||||
ImGui::SameLine(ImGui::GetWindowWidth() - eqSpdW - 16.0f);
|
||||
ImGui::Text("%s", eqSpd);
|
||||
ImGui::TextDisabled("(%.1f damage per second)", dps);
|
||||
}
|
||||
if (eq->item.armor > 0) {
|
||||
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());
|
||||
}
|
||||
// Extra stats for the equipped item
|
||||
for (const auto& es : eq->item.extraStats) {
|
||||
const char* nm = nullptr;
|
||||
switch (es.statType) {
|
||||
case 12: nm = "Defense Rating"; break;
|
||||
case 13: nm = "Dodge Rating"; break;
|
||||
case 14: nm = "Parry Rating"; break;
|
||||
case 16: case 17: case 18: case 31: nm = "Hit Rating"; break;
|
||||
case 19: case 20: case 21: case 32: nm = "Critical Strike Rating"; break;
|
||||
case 28: case 29: case 30: case 35: nm = "Haste Rating"; break;
|
||||
case 34: nm = "Resilience Rating"; break;
|
||||
case 36: nm = "Expertise Rating"; break;
|
||||
case 37: nm = "Attack Power"; break;
|
||||
case 38: nm = "Ranged Attack Power"; break;
|
||||
case 45: nm = "Spell Power"; break;
|
||||
case 46: nm = "Healing Power"; break;
|
||||
case 49: nm = "Mana per 5 sec."; break;
|
||||
default: break;
|
||||
}
|
||||
if (nm && es.statValue > 0)
|
||||
ImGui::TextColored(green, "+%d %s", es.statValue, nm);
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
|
||||
// ---- Main segment renderer ----
|
||||
|
||||
void ChatMarkupRenderer::render(
|
||||
|
|
@ -570,7 +58,7 @@ void ChatMarkupRenderer::render(
|
|||
ImGui::Image((ImTextureID)(uintptr_t)chatIcon, ImVec2(12, 12));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
renderItemTooltip(itemEntry, *ctx.gameHandler, *ctx.inventory, ctx.assetMgr);
|
||||
ItemTooltipRenderer::render(itemEntry, *ctx.gameHandler, *ctx.inventory, ctx.assetMgr);
|
||||
}
|
||||
ImGui::SameLine(0, 2);
|
||||
}
|
||||
|
|
@ -583,7 +71,7 @@ void ChatMarkupRenderer::render(
|
|||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (itemEntry > 0 && ctx.gameHandler && ctx.inventory) {
|
||||
renderItemTooltip(itemEntry, *ctx.gameHandler, *ctx.inventory, ctx.assetMgr);
|
||||
ItemTooltipRenderer::render(itemEntry, *ctx.gameHandler, *ctx.inventory, ctx.assetMgr);
|
||||
}
|
||||
}
|
||||
// Shift-click: insert entire link back into chat input
|
||||
|
|
|
|||
|
|
@ -1,40 +1,15 @@
|
|||
#include "ui/chat_panel.hpp"
|
||||
#include "ui/ui_colors.hpp"
|
||||
#include "rendering/vk_context.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
#include "rendering/camera_controller.hpp"
|
||||
#include "audio/audio_coordinator.hpp"
|
||||
#include "audio/ui_sound_manager.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "pipeline/dbc_layout.hpp"
|
||||
#include "game/expansion_profile.hpp"
|
||||
// chat_utils.cpp — Shared chat utility functions.
|
||||
// Extracted from chat_panel_utils.cpp (Phase 6.6 of chat_panel_ref.md).
|
||||
|
||||
#include "ui/chat/chat_utils.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "game/character.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <imgui.h>
|
||||
#include <imgui_internal.h>
|
||||
#include "core/coordinates.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <sstream>
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
using namespace wowee::ui::colors;
|
||||
constexpr auto& kColorRed = kRed;
|
||||
constexpr auto& kColorBrightGreen= kBrightGreen;
|
||||
constexpr auto& kColorYellow = kYellow;
|
||||
} // namespace
|
||||
namespace wowee { namespace ui { namespace chat_utils {
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
// getChatTypeName / getChatTypeColor moved to ChatTabManager (Phase 1.3)
|
||||
|
||||
|
||||
std::string ChatPanel::replaceGenderPlaceholders(const std::string& text, game::GameHandler& gameHandler) {
|
||||
std::string replaceGenderPlaceholders(const std::string& text,
|
||||
game::GameHandler& gameHandler) {
|
||||
// Get player gender, pronouns, and name
|
||||
game::Gender gender = game::Gender::NONBINARY;
|
||||
std::string playerName = "Adventurer";
|
||||
|
|
@ -50,7 +25,7 @@ std::string ChatPanel::replaceGenderPlaceholders(const std::string& text, game::
|
|||
std::string result = text;
|
||||
|
||||
// Helper to trim whitespace
|
||||
auto trim = [](std::string& s) {
|
||||
auto trimStr = [](std::string& s) {
|
||||
const char* ws = " \t\n\r";
|
||||
size_t start = s.find_first_not_of(ws);
|
||||
if (start == std::string::npos) { s.clear(); return; }
|
||||
|
|
@ -76,46 +51,32 @@ std::string ChatPanel::replaceGenderPlaceholders(const std::string& text, game::
|
|||
size_t colonPos;
|
||||
while ((colonPos = placeholder.find(':', start)) != std::string::npos) {
|
||||
std::string part = placeholder.substr(start, colonPos - start);
|
||||
trim(part);
|
||||
trimStr(part);
|
||||
parts.push_back(part);
|
||||
start = colonPos + 1;
|
||||
}
|
||||
// Add the last part
|
||||
std::string lastPart = placeholder.substr(start);
|
||||
trim(lastPart);
|
||||
trimStr(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;
|
||||
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::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;
|
||||
}
|
||||
|
|
@ -177,53 +138,20 @@ std::string ChatPanel::replaceGenderPlaceholders(const std::string& text, game::
|
|||
return result;
|
||||
}
|
||||
|
||||
// renderBubbles delegates to ChatBubbleManager (Phase 1.4)
|
||||
void ChatPanel::renderBubbles(game::GameHandler& gameHandler) {
|
||||
bubbleManager_.render(gameHandler, services_);
|
||||
}
|
||||
|
||||
|
||||
// ---- Public interface methods ----
|
||||
|
||||
// setupCallbacks delegates to ChatBubbleManager (Phase 1.4)
|
||||
void ChatPanel::setupCallbacks(game::GameHandler& gameHandler) {
|
||||
bubbleManager_.setupCallback(gameHandler);
|
||||
}
|
||||
|
||||
void ChatPanel::insertChatLink(const std::string& link) {
|
||||
if (link.empty()) return;
|
||||
size_t curLen = strlen(chatInputBuffer_);
|
||||
if (curLen + link.size() + 1 < sizeof(chatInputBuffer_)) {
|
||||
strncat(chatInputBuffer_, link.c_str(), sizeof(chatInputBuffer_) - curLen - 1);
|
||||
chatInputMoveCursorToEnd_ = true;
|
||||
refocusChatInput_ = true;
|
||||
std::string getEntityDisplayName(const std::shared_ptr<game::Entity>& entity) {
|
||||
if (entity->getType() == game::ObjectType::PLAYER) {
|
||||
auto player = std::static_pointer_cast<game::Player>(entity);
|
||||
if (!player->getName().empty()) return player->getName();
|
||||
} else if (entity->getType() == game::ObjectType::UNIT) {
|
||||
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
||||
if (!unit->getName().empty()) return unit->getName();
|
||||
} else if (entity->getType() == game::ObjectType::GAMEOBJECT) {
|
||||
auto go = std::static_pointer_cast<game::GameObject>(entity);
|
||||
if (!go->getName().empty()) return go->getName();
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
void ChatPanel::activateSlashInput() {
|
||||
refocusChatInput_ = true;
|
||||
chatInputBuffer_[0] = '/';
|
||||
chatInputBuffer_[1] = '\0';
|
||||
chatInputMoveCursorToEnd_ = true;
|
||||
}
|
||||
|
||||
void ChatPanel::activateInput() {
|
||||
if (chatInputCooldown_ > 0) return; // suppress re-activation right after send
|
||||
refocusChatInput_ = true;
|
||||
}
|
||||
|
||||
void ChatPanel::setWhisperTarget(const std::string& name) {
|
||||
selectedChatType_ = 4; // WHISPER
|
||||
strncpy(whisperTargetBuffer_, name.c_str(), sizeof(whisperTargetBuffer_) - 1);
|
||||
whisperTargetBuffer_[sizeof(whisperTargetBuffer_) - 1] = '\0';
|
||||
refocusChatInput_ = true;
|
||||
}
|
||||
|
||||
ChatPanel::SlashCommands ChatPanel::consumeSlashCommands() {
|
||||
SlashCommands result = slashCmds_;
|
||||
slashCmds_ = {};
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace chat_utils
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
// /startattack, /stopattack, /stopcasting, /cancelqueuedspell
|
||||
// Moved from ChatPanel::sendChatMessage() if/else chain (Phase 3).
|
||||
#include "ui/chat/i_chat_command.hpp"
|
||||
#include "ui/chat/macro_evaluator.hpp"
|
||||
#include "ui/chat_panel.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "game/inventory.hpp"
|
||||
|
|
@ -12,11 +13,6 @@
|
|||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
// Forward declaration of evaluateMacroConditionals (still in chat_panel_commands.cpp)
|
||||
std::string evaluateMacroConditionals(const std::string& rawArg,
|
||||
game::GameHandler& gameHandler,
|
||||
uint64_t& targetOverride);
|
||||
|
||||
// --------------- helpers (local to this TU) ---------------
|
||||
namespace {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// System commands: /run, /dump, /reload, /stopmacro, /clear, /logout
|
||||
// Moved from ChatPanel::sendChatMessage() if/else chain (Phase 3).
|
||||
#include "ui/chat/i_chat_command.hpp"
|
||||
#include "ui/chat/macro_evaluator.hpp"
|
||||
#include "ui/chat_panel.hpp"
|
||||
#include "ui/ui_services.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
|
|
@ -11,11 +12,6 @@
|
|||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
// Forward declaration of evaluateMacroConditionals (still in chat_panel_commands.cpp)
|
||||
std::string evaluateMacroConditionals(const std::string& rawArg,
|
||||
game::GameHandler& gameHandler,
|
||||
uint64_t& targetOverride);
|
||||
|
||||
// --- /run, /script ---
|
||||
class RunCommand : public IChatCommand {
|
||||
public:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
// /focus, /clearfocus
|
||||
// Moved from ChatPanel::sendChatMessage() if/else chain (Phase 3).
|
||||
#include "ui/chat/i_chat_command.hpp"
|
||||
#include "ui/chat/macro_evaluator.hpp"
|
||||
#include "ui/chat_panel.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "game/entity.hpp"
|
||||
|
|
@ -13,11 +14,6 @@
|
|||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
// Forward declaration of evaluateMacroConditionals (still in chat_panel_commands.cpp)
|
||||
std::string evaluateMacroConditionals(const std::string& rawArg,
|
||||
game::GameHandler& gameHandler,
|
||||
uint64_t& targetOverride);
|
||||
|
||||
namespace {
|
||||
|
||||
// Trim leading/trailing whitespace.
|
||||
|
|
|
|||
521
src/ui/chat/item_tooltip_renderer.cpp
Normal file
521
src/ui/chat/item_tooltip_renderer.cpp
Normal file
|
|
@ -0,0 +1,521 @@
|
|||
// ItemTooltipRenderer — renders full WoW-style item tooltips via ImGui.
|
||||
// Extracted from ChatMarkupRenderer::renderItemTooltip (Phase 6.7).
|
||||
#include "ui/chat/item_tooltip_renderer.hpp"
|
||||
#include "ui/ui_colors.hpp"
|
||||
#include "ui/inventory_screen.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "pipeline/dbc_layout.hpp"
|
||||
#include <imgui.h>
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
#include <cstdio>
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
void ItemTooltipRenderer::render(
|
||||
uint32_t itemEntry,
|
||||
game::GameHandler& gameHandler,
|
||||
InventoryScreen& inventoryScreen,
|
||||
pipeline::AssetManager* assetMgr)
|
||||
{
|
||||
const auto* info = gameHandler.getItemInfo(itemEntry);
|
||||
if (!info || !info->valid) return;
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
auto isWeaponInventoryType = [](uint32_t invType) {
|
||||
switch (invType) {
|
||||
case 13: case 15: case 17: case 21: case 25: case 26: return true;
|
||||
default: return false;
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
ImGui::BeginTooltip();
|
||||
// Quality color for name
|
||||
auto qColor = ui::getQualityColor(static_cast<game::ItemQuality>(info->quality));
|
||||
ImGui::TextColored(qColor, "%s", info->name.c_str());
|
||||
|
||||
// Heroic indicator (green, matches WoW tooltip style)
|
||||
constexpr uint32_t kFlagHeroic = 0x8;
|
||||
constexpr uint32_t kFlagUniqueEquipped = 0x1000000;
|
||||
if (info->itemFlags & kFlagHeroic)
|
||||
ImGui::TextColored(ImVec4(0.0f, 0.8f, 0.0f, 1.0f), "Heroic");
|
||||
|
||||
// Bind type (appears right under name in WoW)
|
||||
switch (info->bindType) {
|
||||
case 1: ImGui::TextDisabled("Binds when picked up"); break;
|
||||
case 2: ImGui::TextDisabled("Binds when equipped"); break;
|
||||
case 3: ImGui::TextDisabled("Binds when used"); break;
|
||||
case 4: ImGui::TextDisabled("Quest Item"); break;
|
||||
}
|
||||
// Unique / Unique-Equipped
|
||||
if (info->maxCount == 1)
|
||||
ImGui::TextColored(ui::colors::kTooltipGold, "Unique");
|
||||
else if (info->itemFlags & kFlagUniqueEquipped)
|
||||
ImGui::TextColored(ui::colors::kTooltipGold, "Unique-Equipped");
|
||||
|
||||
// Slot type
|
||||
if (info->inventoryType > 0) {
|
||||
const char* slotName = ui::getInventorySlotName(info->inventoryType);
|
||||
if (slotName[0]) {
|
||||
if (!info->subclassName.empty())
|
||||
ImGui::TextColored(ui::colors::kLightGray, "%s %s", slotName, info->subclassName.c_str());
|
||||
else
|
||||
ImGui::TextColored(ui::colors::kLightGray, "%s", slotName);
|
||||
}
|
||||
}
|
||||
|
||||
const bool isWeapon = isWeaponInventoryType(info->inventoryType);
|
||||
|
||||
// Item level (after slot/subclass)
|
||||
if (info->itemLevel > 0)
|
||||
ImGui::TextDisabled("Item Level %u", info->itemLevel);
|
||||
|
||||
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;
|
||||
char dmgBuf[64], spdBuf[32];
|
||||
std::snprintf(dmgBuf, sizeof(dmgBuf), "%d - %d Damage",
|
||||
static_cast<int>(info->damageMin), static_cast<int>(info->damageMax));
|
||||
std::snprintf(spdBuf, sizeof(spdBuf), "Speed %.2f", speed);
|
||||
float spdW = ImGui::CalcTextSize(spdBuf).x;
|
||||
ImGui::Text("%s", dmgBuf);
|
||||
ImGui::SameLine(ImGui::GetWindowWidth() - spdW - 16.0f);
|
||||
ImGui::Text("%s", spdBuf);
|
||||
ImGui::TextDisabled("(%.1f damage per second)", dps);
|
||||
}
|
||||
ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f);
|
||||
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());
|
||||
}
|
||||
if (info->armor > 0) {
|
||||
ImGui::Text("%d Armor", info->armor);
|
||||
}
|
||||
// Elemental resistances
|
||||
{
|
||||
const int32_t resVals[6] = {
|
||||
info->holyRes, info->fireRes, info->natureRes,
|
||||
info->frostRes, info->shadowRes, info->arcaneRes
|
||||
};
|
||||
static constexpr const char* resLabels[6] = {
|
||||
"Holy Resistance", "Fire Resistance", "Nature Resistance",
|
||||
"Frost Resistance", "Shadow Resistance", "Arcane Resistance"
|
||||
};
|
||||
for (int ri = 0; ri < 6; ++ri)
|
||||
if (resVals[ri] > 0) ImGui::Text("+%d %s", resVals[ri], resLabels[ri]);
|
||||
}
|
||||
// Extra stats (hit/crit/haste/sp/ap/expertise/resilience/etc.)
|
||||
if (!info->extraStats.empty()) {
|
||||
auto statName = [](uint32_t t) -> const char* {
|
||||
switch (t) {
|
||||
case 12: return "Defense Rating";
|
||||
case 13: return "Dodge Rating";
|
||||
case 14: return "Parry Rating";
|
||||
case 15: return "Block Rating";
|
||||
case 16: case 17: case 18: case 31: return "Hit Rating";
|
||||
case 19: case 20: case 21: case 32: return "Critical Strike Rating";
|
||||
case 28: case 29: case 30: case 35: return "Haste Rating";
|
||||
case 34: return "Resilience Rating";
|
||||
case 36: return "Expertise Rating";
|
||||
case 37: return "Attack Power";
|
||||
case 38: return "Ranged Attack Power";
|
||||
case 45: return "Spell Power";
|
||||
case 46: return "Healing Power";
|
||||
case 47: return "Spell Damage";
|
||||
case 49: return "Mana per 5 sec.";
|
||||
case 43: return "Spell Penetration";
|
||||
case 44: return "Block Value";
|
||||
default: return nullptr;
|
||||
}
|
||||
};
|
||||
for (const auto& es : info->extraStats) {
|
||||
const char* nm = statName(es.statType);
|
||||
if (nm && es.statValue > 0)
|
||||
ImGui::TextColored(green, "+%d %s", es.statValue, nm);
|
||||
}
|
||||
}
|
||||
// Gem sockets
|
||||
{
|
||||
const auto& kSocketTypes = ui::kSocketTypes;
|
||||
bool hasSocket = false;
|
||||
for (int s = 0; s < 3; ++s) {
|
||||
if (info->socketColor[s] == 0) continue;
|
||||
if (!hasSocket) { ImGui::Spacing(); hasSocket = true; }
|
||||
for (const auto& st : kSocketTypes) {
|
||||
if (info->socketColor[s] & st.mask) {
|
||||
ImGui::TextColored(st.col, "%s", st.label);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasSocket && info->socketBonus != 0) {
|
||||
static std::unordered_map<uint32_t, std::string> s_enchantNames;
|
||||
static bool s_enchantNamesLoaded = false;
|
||||
if (!s_enchantNamesLoaded && assetMgr) {
|
||||
s_enchantNamesLoaded = true;
|
||||
auto dbc = assetMgr->loadDBC("SpellItemEnchantment.dbc");
|
||||
if (dbc && dbc->isLoaded()) {
|
||||
const auto* lay = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("SpellItemEnchantment") : nullptr;
|
||||
uint32_t nameField = lay ? lay->field("Name") : 8u;
|
||||
if (nameField == 0xFFFFFFFF) nameField = 8;
|
||||
uint32_t fc = dbc->getFieldCount();
|
||||
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
||||
uint32_t eid = dbc->getUInt32(r, 0);
|
||||
if (eid == 0 || nameField >= fc) continue;
|
||||
std::string ename = dbc->getString(r, nameField);
|
||||
if (!ename.empty()) s_enchantNames[eid] = std::move(ename);
|
||||
}
|
||||
}
|
||||
}
|
||||
auto enchIt = s_enchantNames.find(info->socketBonus);
|
||||
if (enchIt != s_enchantNames.end())
|
||||
ImGui::TextColored(colors::kSocketGreen, "Socket Bonus: %s", enchIt->second.c_str());
|
||||
else
|
||||
ImGui::TextColored(colors::kSocketGreen, "Socket Bonus: (id %u)", info->socketBonus);
|
||||
}
|
||||
}
|
||||
// Item set membership
|
||||
if (info->itemSetId != 0) {
|
||||
struct SetEntry {
|
||||
std::string name;
|
||||
std::array<uint32_t, 10> itemIds{};
|
||||
std::array<uint32_t, 10> spellIds{};
|
||||
std::array<uint32_t, 10> thresholds{};
|
||||
};
|
||||
static std::unordered_map<uint32_t, SetEntry> s_setData;
|
||||
static bool s_setDataLoaded = false;
|
||||
if (!s_setDataLoaded && assetMgr) {
|
||||
s_setDataLoaded = true;
|
||||
auto dbc = assetMgr->loadDBC("ItemSet.dbc");
|
||||
if (dbc && dbc->isLoaded()) {
|
||||
const auto* layout = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("ItemSet") : nullptr;
|
||||
auto lf = [&](const char* k, uint32_t def) -> uint32_t {
|
||||
return layout ? (*layout)[k] : def;
|
||||
};
|
||||
uint32_t idF = lf("ID", 0), nameF = lf("Name", 1);
|
||||
const auto& itemKeys = ui::kItemSetItemKeys;
|
||||
const auto& spellKeys = ui::kItemSetSpellKeys;
|
||||
const auto& thrKeys = ui::kItemSetThresholdKeys;
|
||||
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
||||
uint32_t id = dbc->getUInt32(r, idF);
|
||||
if (!id) continue;
|
||||
SetEntry e;
|
||||
e.name = dbc->getString(r, nameF);
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
e.itemIds[i] = dbc->getUInt32(r, layout ? (*layout)[itemKeys[i]] : uint32_t(18 + i));
|
||||
e.spellIds[i] = dbc->getUInt32(r, layout ? (*layout)[spellKeys[i]] : uint32_t(28 + i));
|
||||
e.thresholds[i] = dbc->getUInt32(r, layout ? (*layout)[thrKeys[i]] : uint32_t(38 + i));
|
||||
}
|
||||
s_setData[id] = std::move(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::Spacing();
|
||||
const auto& inv = gameHandler.getInventory();
|
||||
auto setIt = s_setData.find(info->itemSetId);
|
||||
if (setIt != s_setData.end()) {
|
||||
const SetEntry& se = setIt->second;
|
||||
int equipped = 0, total = 0;
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
if (se.itemIds[i] == 0) continue;
|
||||
++total;
|
||||
for (int sl = 0; sl < game::Inventory::NUM_EQUIP_SLOTS; sl++) {
|
||||
const auto& eq = inv.getEquipSlot(static_cast<game::EquipSlot>(sl));
|
||||
if (!eq.empty() && eq.item.itemId == se.itemIds[i]) { ++equipped; break; }
|
||||
}
|
||||
}
|
||||
if (total > 0)
|
||||
ImGui::TextColored(ui::colors::kTooltipGold,
|
||||
"%s (%d/%d)", se.name.empty() ? "Set" : se.name.c_str(), equipped, total);
|
||||
else if (!se.name.empty())
|
||||
ImGui::TextColored(ui::colors::kTooltipGold, "%s", se.name.c_str());
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
if (se.spellIds[i] == 0 || se.thresholds[i] == 0) continue;
|
||||
const std::string& bname = gameHandler.getSpellName(se.spellIds[i]);
|
||||
bool active = (equipped >= static_cast<int>(se.thresholds[i]));
|
||||
ImVec4 col = active ? colors::kActiveGreen : colors::kInactiveGray;
|
||||
if (!bname.empty())
|
||||
ImGui::TextColored(col, "(%u) %s", se.thresholds[i], bname.c_str());
|
||||
else
|
||||
ImGui::TextColored(col, "(%u) Set Bonus", se.thresholds[i]);
|
||||
}
|
||||
} else {
|
||||
ImGui::TextColored(ui::colors::kTooltipGold, "Set (id %u)", info->itemSetId);
|
||||
}
|
||||
}
|
||||
// Item spell effects (Use / Equip / Chance on Hit / Teaches)
|
||||
for (const auto& sp : info->spells) {
|
||||
if (sp.spellId == 0) continue;
|
||||
const char* triggerLabel = nullptr;
|
||||
switch (sp.spellTrigger) {
|
||||
case 0: triggerLabel = "Use"; break;
|
||||
case 1: triggerLabel = "Equip"; break;
|
||||
case 2: triggerLabel = "Chance on Hit"; break;
|
||||
case 5: triggerLabel = "Teaches"; break;
|
||||
}
|
||||
if (!triggerLabel) continue;
|
||||
const std::string& spDesc = gameHandler.getSpellDescription(sp.spellId);
|
||||
const std::string& spText = !spDesc.empty() ? spDesc
|
||||
: gameHandler.getSpellName(sp.spellId);
|
||||
if (!spText.empty()) {
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 300.0f);
|
||||
ImGui::TextColored(colors::kCyan,
|
||||
"%s: %s", triggerLabel, spText.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
}
|
||||
// Required level
|
||||
if (info->requiredLevel > 1)
|
||||
ImGui::TextDisabled("Requires Level %u", info->requiredLevel);
|
||||
// Required skill
|
||||
if (info->requiredSkill != 0 && info->requiredSkillRank > 0) {
|
||||
static std::unordered_map<uint32_t, std::string> s_skillNames;
|
||||
static bool s_skillNamesLoaded = false;
|
||||
if (!s_skillNamesLoaded && assetMgr) {
|
||||
s_skillNamesLoaded = true;
|
||||
auto dbc = assetMgr->loadDBC("SkillLine.dbc");
|
||||
if (dbc && dbc->isLoaded()) {
|
||||
const auto* layout = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("SkillLine") : nullptr;
|
||||
uint32_t idF = layout ? (*layout)["ID"] : 0u;
|
||||
uint32_t nameF = layout ? (*layout)["Name"] : 2u;
|
||||
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
||||
uint32_t sid = dbc->getUInt32(r, idF);
|
||||
if (!sid) continue;
|
||||
std::string sname = dbc->getString(r, nameF);
|
||||
if (!sname.empty()) s_skillNames[sid] = std::move(sname);
|
||||
}
|
||||
}
|
||||
}
|
||||
uint32_t playerSkillVal = 0;
|
||||
const auto& skills = gameHandler.getPlayerSkills();
|
||||
auto skPit = skills.find(info->requiredSkill);
|
||||
if (skPit != skills.end()) playerSkillVal = skPit->second.effectiveValue();
|
||||
bool meetsSkill = (playerSkillVal == 0 || playerSkillVal >= info->requiredSkillRank);
|
||||
ImVec4 skColor = meetsSkill ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : colors::kPaleRed;
|
||||
auto skIt = s_skillNames.find(info->requiredSkill);
|
||||
if (skIt != s_skillNames.end())
|
||||
ImGui::TextColored(skColor, "Requires %s (%u)", skIt->second.c_str(), info->requiredSkillRank);
|
||||
else
|
||||
ImGui::TextColored(skColor, "Requires Skill %u (%u)", info->requiredSkill, info->requiredSkillRank);
|
||||
}
|
||||
// Required reputation
|
||||
if (info->requiredReputationFaction != 0 && info->requiredReputationRank > 0) {
|
||||
static std::unordered_map<uint32_t, std::string> s_factionNames;
|
||||
static bool s_factionNamesLoaded = false;
|
||||
if (!s_factionNamesLoaded && assetMgr) {
|
||||
s_factionNamesLoaded = true;
|
||||
auto dbc = assetMgr->loadDBC("Faction.dbc");
|
||||
if (dbc && dbc->isLoaded()) {
|
||||
const auto* layout = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("Faction") : nullptr;
|
||||
uint32_t idF = layout ? (*layout)["ID"] : 0u;
|
||||
uint32_t nameF = layout ? (*layout)["Name"] : 20u;
|
||||
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
||||
uint32_t fid = dbc->getUInt32(r, idF);
|
||||
if (!fid) continue;
|
||||
std::string fname = dbc->getString(r, nameF);
|
||||
if (!fname.empty()) s_factionNames[fid] = std::move(fname);
|
||||
}
|
||||
}
|
||||
}
|
||||
static constexpr const char* kRepRankNames[] = {
|
||||
"Hated", "Hostile", "Unfriendly", "Neutral",
|
||||
"Friendly", "Honored", "Revered", "Exalted"
|
||||
};
|
||||
const char* rankName = (info->requiredReputationRank < 8)
|
||||
? kRepRankNames[info->requiredReputationRank] : "Unknown";
|
||||
auto fIt = s_factionNames.find(info->requiredReputationFaction);
|
||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.75f), "Requires %s with %s",
|
||||
rankName,
|
||||
fIt != s_factionNames.end() ? fIt->second.c_str() : "Unknown Faction");
|
||||
}
|
||||
// Class restriction
|
||||
if (info->allowableClass != 0) {
|
||||
const auto& kClasses = ui::kClassMasks;
|
||||
int matchCount = 0;
|
||||
for (const auto& kc : kClasses)
|
||||
if (info->allowableClass & kc.mask) ++matchCount;
|
||||
if (matchCount > 0 && matchCount < 10) {
|
||||
char classBuf[128] = "Classes: ";
|
||||
bool first = true;
|
||||
for (const auto& kc : kClasses) {
|
||||
if (!(info->allowableClass & kc.mask)) continue;
|
||||
if (!first) strncat(classBuf, ", ", sizeof(classBuf) - strlen(classBuf) - 1);
|
||||
strncat(classBuf, kc.name, sizeof(classBuf) - strlen(classBuf) - 1);
|
||||
first = false;
|
||||
}
|
||||
uint8_t pc = gameHandler.getPlayerClass();
|
||||
uint32_t pmask = (pc > 0 && pc <= 10) ? (1u << (pc - 1)) : 0u;
|
||||
bool playerAllowed = (pmask == 0 || (info->allowableClass & pmask));
|
||||
ImVec4 clColor = playerAllowed ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : colors::kPaleRed;
|
||||
ImGui::TextColored(clColor, "%s", classBuf);
|
||||
}
|
||||
}
|
||||
// Race restriction
|
||||
if (info->allowableRace != 0) {
|
||||
const auto& kRaces = ui::kRaceMasks;
|
||||
constexpr uint32_t kAllPlayable = 1|2|4|8|16|32|64|128|512|1024;
|
||||
if ((info->allowableRace & kAllPlayable) != kAllPlayable) {
|
||||
int matchCount = 0;
|
||||
for (const auto& kr : kRaces)
|
||||
if (info->allowableRace & kr.mask) ++matchCount;
|
||||
if (matchCount > 0) {
|
||||
char raceBuf[160] = "Races: ";
|
||||
bool first = true;
|
||||
for (const auto& kr : kRaces) {
|
||||
if (!(info->allowableRace & kr.mask)) continue;
|
||||
if (!first) strncat(raceBuf, ", ", sizeof(raceBuf) - strlen(raceBuf) - 1);
|
||||
strncat(raceBuf, kr.name, sizeof(raceBuf) - strlen(raceBuf) - 1);
|
||||
first = false;
|
||||
}
|
||||
uint8_t pr = gameHandler.getPlayerRace();
|
||||
uint32_t pmask = (pr > 0 && pr <= 11) ? (1u << (pr - 1)) : 0u;
|
||||
bool playerAllowed = (pmask == 0 || (info->allowableRace & pmask));
|
||||
ImVec4 rColor = playerAllowed ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : colors::kPaleRed;
|
||||
ImGui::TextColored(rColor, "%s", raceBuf);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Flavor text
|
||||
if (!info->description.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::PushTextWrapPos(300.0f);
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 0.85f), "\"%s\"", info->description.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
if (info->sellPrice > 0) {
|
||||
ImGui::TextDisabled("Sell:"); ImGui::SameLine(0, 4);
|
||||
renderCoinsFromCopper(info->sellPrice);
|
||||
}
|
||||
|
||||
if (ImGui::GetIO().KeyShift && info->inventoryType > 0) {
|
||||
if (const auto* eq = findComparableEquipped(static_cast<uint8_t>(info->inventoryType))) {
|
||||
ImGui::Separator();
|
||||
ImGui::TextDisabled("Equipped:");
|
||||
VkDescriptorSet eqIcon = inventoryScreen.getItemIcon(eq->item.displayInfoId);
|
||||
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());
|
||||
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;
|
||||
char eqDmg[64], eqSpd[32];
|
||||
std::snprintf(eqDmg, sizeof(eqDmg), "%d - %d Damage",
|
||||
static_cast<int>(eq->item.damageMin), static_cast<int>(eq->item.damageMax));
|
||||
std::snprintf(eqSpd, sizeof(eqSpd), "Speed %.2f", speed);
|
||||
float eqSpdW = ImGui::CalcTextSize(eqSpd).x;
|
||||
ImGui::Text("%s", eqDmg);
|
||||
ImGui::SameLine(ImGui::GetWindowWidth() - eqSpdW - 16.0f);
|
||||
ImGui::Text("%s", eqSpd);
|
||||
ImGui::TextDisabled("(%.1f damage per second)", dps);
|
||||
}
|
||||
if (eq->item.armor > 0) {
|
||||
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());
|
||||
}
|
||||
// Extra stats for the equipped item
|
||||
for (const auto& es : eq->item.extraStats) {
|
||||
const char* nm = nullptr;
|
||||
switch (es.statType) {
|
||||
case 12: nm = "Defense Rating"; break;
|
||||
case 13: nm = "Dodge Rating"; break;
|
||||
case 14: nm = "Parry Rating"; break;
|
||||
case 16: case 17: case 18: case 31: nm = "Hit Rating"; break;
|
||||
case 19: case 20: case 21: case 32: nm = "Critical Strike Rating"; break;
|
||||
case 28: case 29: case 30: case 35: nm = "Haste Rating"; break;
|
||||
case 34: nm = "Resilience Rating"; break;
|
||||
case 36: nm = "Expertise Rating"; break;
|
||||
case 37: nm = "Attack Power"; break;
|
||||
case 38: nm = "Ranged Attack Power"; break;
|
||||
case 45: nm = "Spell Power"; break;
|
||||
case 46: nm = "Healing Power"; break;
|
||||
case 49: nm = "Mana per 5 sec."; break;
|
||||
default: break;
|
||||
}
|
||||
if (nm && es.statValue > 0)
|
||||
ImGui::TextColored(green, "+%d %s", es.statValue, nm);
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
24
src/ui/chat/macro_eval_convenience.cpp
Normal file
24
src/ui/chat/macro_eval_convenience.cpp
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// evaluateMacroConditionals — convenience free function.
|
||||
// Thin wrapper over MacroEvaluator with concrete adapters.
|
||||
// Separate TU to avoid pulling Application/Renderer into macro_evaluator unit tests.
|
||||
#include "ui/chat/macro_evaluator.hpp"
|
||||
#include "ui/chat/game_state_adapter.hpp"
|
||||
#include "ui/chat/input_modifier_adapter.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
std::string evaluateMacroConditionals(const std::string& rawArg,
|
||||
game::GameHandler& gameHandler,
|
||||
uint64_t& targetOverride) {
|
||||
auto* renderer = core::Application::getInstance().getRenderer();
|
||||
GameStateAdapter gs(gameHandler, renderer);
|
||||
InputModifierAdapter im;
|
||||
MacroEvaluator eval(gs, im);
|
||||
return eval.evaluate(rawArg, targetOverride);
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,359 +0,0 @@
|
|||
#include "ui/chat_panel.hpp"
|
||||
#include "ui/chat/macro_evaluator.hpp"
|
||||
#include "ui/chat/game_state_adapter.hpp"
|
||||
#include "ui/chat/input_modifier_adapter.hpp"
|
||||
#include "ui/chat/chat_utils.hpp"
|
||||
#include "ui/chat/gm_command_data.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include "addons/addon_manager.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "rendering/animation_controller.hpp"
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <cctype>
|
||||
|
||||
using wowee::ui::chat_utils::trim;
|
||||
using wowee::ui::chat_utils::toLower;
|
||||
|
||||
namespace {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
static std::vector<std::string> allMacroCommands(const std::string& macroText) {
|
||||
std::vector<std::string> cmds;
|
||||
size_t pos = 0;
|
||||
while (pos <= macroText.size()) {
|
||||
size_t nl = macroText.find('\n', pos);
|
||||
std::string line = (nl != std::string::npos) ? macroText.substr(pos, nl - pos) : macroText.substr(pos);
|
||||
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||||
size_t start = line.find_first_not_of(" \t");
|
||||
if (start != std::string::npos) line = line.substr(start);
|
||||
if (!line.empty() && line.front() != '#')
|
||||
cmds.push_back(std::move(line));
|
||||
if (nl == std::string::npos) break;
|
||||
pos = nl + 1;
|
||||
}
|
||||
return cmds;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// evaluateMacroConditionals — thin wrapper over MacroEvaluator (Phase 4).
|
||||
// Preserved for backward compatibility with command files that forward-declare it.
|
||||
// ---------------------------------------------------------------------------
|
||||
std::string evaluateMacroConditionals(const std::string& rawArg,
|
||||
game::GameHandler& gameHandler,
|
||||
uint64_t& targetOverride) {
|
||||
auto* renderer = core::Application::getInstance().getRenderer();
|
||||
GameStateAdapter gs(gameHandler, renderer);
|
||||
InputModifierAdapter im;
|
||||
MacroEvaluator eval(gs, im);
|
||||
return eval.evaluate(rawArg, targetOverride);
|
||||
}
|
||||
|
||||
// Execute all non-comment lines of a macro body in sequence.
|
||||
// In WoW, every line executes per click; the server enforces spell-cast limits.
|
||||
// /stopmacro (with optional conditionals) halts the remaining commands early.
|
||||
|
||||
void ChatPanel::executeMacroText(game::GameHandler& gameHandler,
|
||||
const std::string& macroText) {
|
||||
macroStopped_ = false;
|
||||
for (const auto& cmd : allMacroCommands(macroText)) {
|
||||
strncpy(chatInputBuffer_, cmd.c_str(), sizeof(chatInputBuffer_) - 1);
|
||||
chatInputBuffer_[sizeof(chatInputBuffer_) - 1] = '\0';
|
||||
sendChatMessage(gameHandler);
|
||||
if (macroStopped_) break;
|
||||
}
|
||||
macroStopped_ = false;
|
||||
}
|
||||
|
||||
// /castsequence state moved to CastSequenceTracker member (Phase 1.5)
|
||||
|
||||
|
||||
void ChatPanel::sendChatMessage(game::GameHandler& gameHandler) {
|
||||
if (strlen(chatInputBuffer_) > 0) {
|
||||
std::string input(chatInputBuffer_);
|
||||
|
||||
// Save to sent-message history (skip pure whitespace, cap at 50 entries)
|
||||
{
|
||||
bool allSpace = true;
|
||||
for (char c : input) { if (!std::isspace(static_cast<unsigned char>(c))) { allSpace = false; break; } }
|
||||
if (!allSpace) {
|
||||
// Remove duplicate of last entry if identical
|
||||
if (chatSentHistory_.empty() || chatSentHistory_.back() != input) {
|
||||
chatSentHistory_.push_back(input);
|
||||
if (chatSentHistory_.size() > 50)
|
||||
chatSentHistory_.erase(chatSentHistory_.begin());
|
||||
}
|
||||
}
|
||||
}
|
||||
chatHistoryIdx_ = -1; // reset browsing position after send
|
||||
|
||||
game::ChatType type = game::ChatType::SAY;
|
||||
std::string message = input;
|
||||
std::string target;
|
||||
|
||||
// GM dot-prefix commands (.gm, .tele, .additem, etc.)
|
||||
// Sent to server as SAY — the server interprets the dot-prefix.
|
||||
// Requires GM security level on the server (account set gmlevel <user> 3 -1).
|
||||
if (input.size() > 1 && input[0] == '.') {
|
||||
LOG_INFO("GM command: '", input, "' — sending as SAY to server");
|
||||
gameHandler.sendChatMessage(game::ChatType::SAY, input, "");
|
||||
|
||||
// Build feedback: check if this is a known command
|
||||
std::string dotCmd = input;
|
||||
size_t sp = dotCmd.find(' ');
|
||||
std::string cmdPart = (sp != std::string::npos)
|
||||
? dotCmd.substr(1, sp - 1) : dotCmd.substr(1);
|
||||
for (char& c : cmdPart) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
|
||||
// Look for a matching entry in the GM command table
|
||||
std::string feedback;
|
||||
for (const auto& entry : kGmCommands) {
|
||||
if (entry.name == cmdPart) {
|
||||
feedback = "Sent: " + input + " (" + std::string(entry.help) + ")";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (feedback.empty())
|
||||
feedback = "Sent: " + input
|
||||
+ " (requires GM access — server console: account set gmlevel <user> 3 -1)";
|
||||
gameHandler.addLocalChatMessage(chat_utils::makeSystemMessage(feedback));
|
||||
chatInputBuffer_[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for slash commands
|
||||
if (input.size() > 1 && input[0] == '/') {
|
||||
std::string command = input.substr(1);
|
||||
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 = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
|
||||
// /run <lua code> — execute Lua script via addon system
|
||||
if ((cmdLower == "run" || cmdLower == "script") && spacePos != std::string::npos) {
|
||||
std::string luaCode = command.substr(spacePos + 1);
|
||||
auto* am = services_.addonManager;
|
||||
if (am) {
|
||||
am->runScript(luaCode);
|
||||
} else {
|
||||
gameHandler.addUIError("Addon system not initialized.");
|
||||
}
|
||||
chatInputBuffer_[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
// /dump <expression> — evaluate Lua expression and print result
|
||||
if ((cmdLower == "dump" || cmdLower == "print") && spacePos != std::string::npos) {
|
||||
std::string expr = command.substr(spacePos + 1);
|
||||
auto* am = services_.addonManager;
|
||||
if (am && am->isInitialized()) {
|
||||
// Wrap expression in print(tostring(...)) to display the value
|
||||
std::string wrapped = "local __v = " + expr +
|
||||
"; if type(__v) == 'table' then "
|
||||
" local parts = {} "
|
||||
" for k,v in pairs(__v) do parts[#parts+1] = tostring(k)..'='..tostring(v) end "
|
||||
" print('{' .. table.concat(parts, ', ') .. '}') "
|
||||
"else print(tostring(__v)) end";
|
||||
am->runScript(wrapped);
|
||||
} else {
|
||||
game::MessageChatData errMsg;
|
||||
errMsg.type = game::ChatType::SYSTEM;
|
||||
errMsg.language = game::ChatLanguage::UNIVERSAL;
|
||||
errMsg.message = "Addon system not initialized.";
|
||||
gameHandler.addLocalChatMessage(errMsg);
|
||||
}
|
||||
chatInputBuffer_[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
// Check addon slash commands (SlashCmdList) before built-in commands
|
||||
{
|
||||
auto* am = services_.addonManager;
|
||||
if (am && am->isInitialized()) {
|
||||
std::string slashCmd = "/" + cmdLower;
|
||||
std::string slashArgs;
|
||||
if (spacePos != std::string::npos) slashArgs = command.substr(spacePos + 1);
|
||||
if (am->getLuaEngine()->dispatchSlashCommand(slashCmd, slashArgs)) {
|
||||
chatInputBuffer_[0] = '\0';
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch through command registry (Phase 3.11)
|
||||
std::string args;
|
||||
if (spacePos != std::string::npos)
|
||||
args = command.substr(spacePos + 1);
|
||||
|
||||
ChatCommandContext ctx{gameHandler, services_, *this, args, cmdLower};
|
||||
ChatCommandResult result = commandRegistry_.dispatch(cmdLower, ctx);
|
||||
|
||||
if (result.handled) {
|
||||
if (result.clearInput)
|
||||
chatInputBuffer_[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
// Emote fallthrough — dynamic DBC lookup for emote text (catch-all).
|
||||
// Not registered in the command registry because emote names are data-driven.
|
||||
{
|
||||
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::AnimationController::getEmoteText(cmdLower, targetNamePtr);
|
||||
if (!emoteText.empty()) {
|
||||
auto* renderer = services_.renderer;
|
||||
if (renderer) {
|
||||
if (auto* ac = renderer->getAnimationController()) ac->playEmote(cmdLower);
|
||||
}
|
||||
|
||||
uint32_t dbcId = rendering::AnimationController::getEmoteDbcId(cmdLower);
|
||||
if (dbcId != 0) {
|
||||
uint64_t targetGuid = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
|
||||
gameHandler.sendTextEmote(dbcId, targetGuid);
|
||||
}
|
||||
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::TEXT_EMOTE;
|
||||
msg.language = game::ChatLanguage::COMMON;
|
||||
msg.message = emoteText;
|
||||
gameHandler.addLocalChatMessage(msg);
|
||||
|
||||
chatInputBuffer_[0] = '\0';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Unrecognized slash command — fall through to dropdown chat type
|
||||
message = input;
|
||||
}
|
||||
|
||||
// Determine chat type from dropdown selection
|
||||
// (reached when: no slash prefix, OR unrecognized slash command)
|
||||
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;
|
||||
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
|
||||
case 10: { // CHANNEL
|
||||
const auto& chans = gameHandler.getJoinedChannels();
|
||||
if (!chans.empty() && selectedChannelIdx_ < static_cast<int>(chans.size())) {
|
||||
type = game::ChatType::CHANNEL;
|
||||
target = chans[selectedChannelIdx_];
|
||||
} else { type = game::ChatType::SAY; }
|
||||
break;
|
||||
}
|
||||
default: type = game::ChatType::SAY; break;
|
||||
}
|
||||
|
||||
// PortBot whisper interception (for dropdown-typed whispers, not /w command)
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Don't send empty messages
|
||||
if (!message.empty()) {
|
||||
gameHandler.sendChatMessage(type, message, target);
|
||||
}
|
||||
|
||||
// Clear input
|
||||
chatInputBuffer_[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
#include "ui/ui_colors.hpp"
|
||||
#include "ui/inventory_screen.hpp"
|
||||
#include "ui/keybinding_manager.hpp"
|
||||
#include "ui/chat/chat_utils.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "core/input.hpp"
|
||||
#include <imgui.h>
|
||||
|
|
@ -10,125 +11,6 @@
|
|||
namespace wowee { namespace ui {
|
||||
|
||||
namespace {
|
||||
// Helper function to replace gender placeholders, pronouns, and name
|
||||
std::string replaceGenderPlaceholders(const std::string& text, game::GameHandler& gameHandler) {
|
||||
game::Gender gender = game::Gender::NONBINARY;
|
||||
std::string playerName = "Adventurer";
|
||||
const auto* character = gameHandler.getActiveCharacter();
|
||||
if (character) {
|
||||
gender = character->gender;
|
||||
if (!character->name.empty()) {
|
||||
playerName = character->name;
|
||||
}
|
||||
}
|
||||
game::Pronouns pronouns = game::Pronouns::forGender(gender);
|
||||
|
||||
std::string result = text;
|
||||
|
||||
auto trim = [](std::string& s) {
|
||||
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);
|
||||
};
|
||||
|
||||
// Replace $g placeholders
|
||||
size_t pos = 0;
|
||||
while ((pos = result.find('$', pos)) != std::string::npos) {
|
||||
if (pos + 1 >= result.length()) break;
|
||||
char marker = result[pos + 1];
|
||||
if (marker != 'g' && marker != 'G') { pos++; continue; }
|
||||
|
||||
size_t endPos = result.find(';', pos);
|
||||
if (endPos == std::string::npos) { pos += 2; continue; }
|
||||
|
||||
std::string placeholder = result.substr(pos + 2, endPos - pos - 2);
|
||||
|
||||
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;
|
||||
}
|
||||
std::string lastPart = placeholder.substr(start);
|
||||
trim(lastPart);
|
||||
parts.push_back(lastPart);
|
||||
|
||||
std::string replacement;
|
||||
if (parts.size() >= 3) {
|
||||
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) {
|
||||
switch (gender) {
|
||||
case game::Gender::MALE: replacement = parts[0]; break;
|
||||
case game::Gender::FEMALE: replacement = parts[1]; break;
|
||||
case game::Gender::NONBINARY:
|
||||
replacement = parts[0].length() <= parts[1].length() ? parts[0] : parts[1];
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
pos = endPos + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
result.replace(pos, endPos - pos + 1, replacement);
|
||||
pos += replacement.length();
|
||||
}
|
||||
|
||||
// Resolve class and race names for $C and $R placeholders
|
||||
std::string className = "Adventurer";
|
||||
std::string raceName = "Unknown";
|
||||
if (character) {
|
||||
className = game::getClassName(character->characterClass);
|
||||
raceName = game::getRaceName(character->race);
|
||||
}
|
||||
|
||||
// Replace simple placeholders
|
||||
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 'c': case 'C': replacement = className; break;
|
||||
case 'r': case 'R': replacement = raceName; 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;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string cleanQuestTitleForUi(const std::string& raw, uint32_t questId) {
|
||||
std::string s = raw;
|
||||
|
|
@ -446,7 +328,7 @@ void QuestLogScreen::render(game::GameHandler& gameHandler, InventoryScreen& inv
|
|||
if (lastDetailRequestQuestId_ == sel.questId) lastDetailRequestQuestId_ = 0;
|
||||
questDetailQueryNoResponse_.erase(sel.questId);
|
||||
ImGui::TextColored(ImVec4(0.82f, 0.9f, 1.0f, 1.0f), "Summary");
|
||||
std::string processedObjectives = replaceGenderPlaceholders(sel.objectives, gameHandler);
|
||||
std::string processedObjectives = chat_utils::replaceGenderPlaceholders(sel.objectives, gameHandler);
|
||||
float textHeight = ImGui::GetContentRegionAvail().y * 0.45f;
|
||||
if (textHeight < 120.0f) textHeight = 120.0f;
|
||||
ImGui::BeginChild("QuestObjectiveText", ImVec2(0, textHeight), true);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
// ============================================================
|
||||
#include "ui/window_manager.hpp"
|
||||
#include "ui/chat_panel.hpp"
|
||||
#include "ui/chat/chat_utils.hpp"
|
||||
#include "ui/settings_panel.hpp"
|
||||
#include "ui/spellbook_screen.hpp"
|
||||
#include "ui/inventory_screen.hpp"
|
||||
|
|
@ -252,7 +253,7 @@ void WindowManager::renderLootWindow(game::GameHandler& gameHandler,
|
|||
}
|
||||
|
||||
void WindowManager::renderGossipWindow(game::GameHandler& gameHandler,
|
||||
ChatPanel& chatPanel) {
|
||||
ChatPanel& /*chatPanel*/) {
|
||||
if (!gameHandler.isGossipWindowOpen()) return;
|
||||
|
||||
auto* window = services_.window;
|
||||
|
|
@ -330,7 +331,7 @@ void WindowManager::renderGossipWindow(game::GameHandler& gameHandler,
|
|||
displayText = placeholderIt->second;
|
||||
}
|
||||
|
||||
std::string processedText = chatPanel.replaceGenderPlaceholders(displayText, gameHandler);
|
||||
std::string processedText = chat_utils::replaceGenderPlaceholders(displayText, gameHandler);
|
||||
std::string label = std::string(icon) + " " + processedText;
|
||||
if (ImGui::Selectable(label.c_str())) {
|
||||
if (opt.text == "GOSSIP_OPTION_ARMORER") {
|
||||
|
|
@ -436,11 +437,11 @@ void WindowManager::renderQuestDetailsWindow(game::GameHandler& gameHandler,
|
|||
|
||||
bool open = true;
|
||||
const auto& quest = gameHandler.getQuestDetails();
|
||||
std::string processedTitle = chatPanel.replaceGenderPlaceholders(quest.title, gameHandler);
|
||||
std::string processedTitle = chat_utils::replaceGenderPlaceholders(quest.title, gameHandler);
|
||||
if (ImGui::Begin(processedTitle.c_str(), &open)) {
|
||||
// Quest description
|
||||
if (!quest.details.empty()) {
|
||||
std::string processedDetails = chatPanel.replaceGenderPlaceholders(quest.details, gameHandler);
|
||||
std::string processedDetails = chat_utils::replaceGenderPlaceholders(quest.details, gameHandler);
|
||||
ImGui::TextWrapped("%s", processedDetails.c_str());
|
||||
}
|
||||
|
||||
|
|
@ -449,7 +450,7 @@ void WindowManager::renderQuestDetailsWindow(game::GameHandler& gameHandler,
|
|||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ui::colors::kTooltipGold, "Objectives:");
|
||||
std::string processedObjectives = chatPanel.replaceGenderPlaceholders(quest.objectives, gameHandler);
|
||||
std::string processedObjectives = chat_utils::replaceGenderPlaceholders(quest.objectives, gameHandler);
|
||||
ImGui::TextWrapped("%s", processedObjectives.c_str());
|
||||
}
|
||||
|
||||
|
|
@ -577,10 +578,10 @@ void WindowManager::renderQuestRequestItemsWindow(game::GameHandler& gameHandler
|
|||
return total;
|
||||
};
|
||||
|
||||
std::string processedTitle = chatPanel.replaceGenderPlaceholders(quest.title, gameHandler);
|
||||
std::string processedTitle = chat_utils::replaceGenderPlaceholders(quest.title, gameHandler);
|
||||
if (ImGui::Begin(processedTitle.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
|
||||
if (!quest.completionText.empty()) {
|
||||
std::string processedCompletionText = chatPanel.replaceGenderPlaceholders(quest.completionText, gameHandler);
|
||||
std::string processedCompletionText = chat_utils::replaceGenderPlaceholders(quest.completionText, gameHandler);
|
||||
ImGui::TextWrapped("%s", processedCompletionText.c_str());
|
||||
}
|
||||
|
||||
|
|
@ -670,10 +671,10 @@ void WindowManager::renderQuestOfferRewardWindow(game::GameHandler& gameHandler,
|
|||
selectedChoice = 0;
|
||||
}
|
||||
|
||||
std::string processedTitle = chatPanel.replaceGenderPlaceholders(quest.title, gameHandler);
|
||||
std::string processedTitle = chat_utils::replaceGenderPlaceholders(quest.title, gameHandler);
|
||||
if (ImGui::Begin(processedTitle.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
|
||||
if (!quest.rewardText.empty()) {
|
||||
std::string processedRewardText = chatPanel.replaceGenderPlaceholders(quest.rewardText, gameHandler);
|
||||
std::string processedRewardText = chat_utils::replaceGenderPlaceholders(quest.rewardText, gameHandler);
|
||||
ImGui::TextWrapped("%s", processedRewardText.c_str());
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue