refactor(chat): decompose into modular architecture, add GM commands, fix protocol

- 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>
This commit is contained in:
Pavel Okhlopkov 2026-04-12 14:59:56 +03:00
parent 09c4a9a04a
commit 42f1bb98ea
54 changed files with 7363 additions and 3856 deletions

View file

@ -259,6 +259,7 @@ bool MessageChatParser::parse(network::Packet& packet, MessageChatData& data) {
const char* getChatTypeString(ChatType type) {
switch (type) {
case ChatType::SYSTEM: return "SYSTEM";
case ChatType::SAY: return "SAY";
case ChatType::PARTY: return "PARTY";
case ChatType::RAID: return "RAID";
@ -266,12 +267,14 @@ const char* getChatTypeString(ChatType type) {
case ChatType::OFFICER: return "OFFICER";
case ChatType::YELL: return "YELL";
case ChatType::WHISPER: return "WHISPER";
case ChatType::WHISPER_FOREIGN: return "WHISPER_FOREIGN";
case ChatType::WHISPER_INFORM: return "WHISPER_INFORM";
case ChatType::EMOTE: return "EMOTE";
case ChatType::TEXT_EMOTE: return "TEXT_EMOTE";
case ChatType::SYSTEM: return "SYSTEM";
case ChatType::MONSTER_SAY: return "MONSTER_SAY";
case ChatType::MONSTER_PARTY: return "MONSTER_PARTY";
case ChatType::MONSTER_YELL: return "MONSTER_YELL";
case ChatType::MONSTER_WHISPER: return "MONSTER_WHISPER";
case ChatType::MONSTER_EMOTE: return "MONSTER_EMOTE";
case ChatType::CHANNEL: return "CHANNEL";
case ChatType::CHANNEL_JOIN: return "CHANNEL_JOIN";
@ -284,12 +287,18 @@ const char* getChatTypeString(ChatType type) {
case ChatType::IGNORED: return "IGNORED";
case ChatType::SKILL: return "SKILL";
case ChatType::LOOT: return "LOOT";
case ChatType::BATTLEGROUND: return "BATTLEGROUND";
case ChatType::BATTLEGROUND_LEADER: return "BATTLEGROUND_LEADER";
case ChatType::BG_SYSTEM_NEUTRAL: return "BG_SYSTEM_NEUTRAL";
case ChatType::BG_SYSTEM_ALLIANCE: return "BG_SYSTEM_ALLIANCE";
case ChatType::BG_SYSTEM_HORDE: return "BG_SYSTEM_HORDE";
case ChatType::RAID_LEADER: return "RAID_LEADER";
case ChatType::RAID_WARNING: return "RAID_WARNING";
case ChatType::RAID_BOSS_EMOTE: return "RAID_BOSS_EMOTE";
case ChatType::RAID_BOSS_WHISPER: return "RAID_BOSS_WHISPER";
case ChatType::BATTLEGROUND: return "BATTLEGROUND";
case ChatType::BATTLEGROUND_LEADER: return "BATTLEGROUND_LEADER";
case ChatType::ACHIEVEMENT: return "ACHIEVEMENT";
case ChatType::GUILD_ACHIEVEMENT: return "GUILD_ACHIEVEMENT";
case ChatType::PARTY_LEADER: return "PARTY_LEADER";
default: return "UNKNOWN";
}
}

View file

@ -166,7 +166,7 @@ void ActionBarPanel::renderActionBar(game::GameHandler& gameHandler,
ChatPanel& chatPanel,
InventoryScreen& inventoryScreen,
SpellbookScreen& spellbookScreen,
QuestLogScreen& questLogScreen,
QuestLogScreen& /*questLogScreen*/,
SpellIconFn getSpellIcon) {
// Use ImGui's display size — always in sync with the current swap-chain/frame,
// whereas window->getWidth/Height() can lag by one frame on resize events.
@ -539,7 +539,7 @@ void ActionBarPanel::renderActionBar(game::GameHandler& gameHandler,
} else if (slot.type == game::ActionBarSlot::ITEM && slot.id != 0) {
gameHandler.useItemById(slot.id);
} else if (slot.type == game::ActionBarSlot::MACRO) {
chatPanel.executeMacroText(gameHandler, inventoryScreen, spellbookScreen, questLogScreen, gameHandler.getMacroText(slot.id));
chatPanel.executeMacroText(gameHandler, gameHandler.getMacroText(slot.id));
}
}
@ -572,7 +572,7 @@ void ActionBarPanel::renderActionBar(game::GameHandler& gameHandler,
ImGui::TextDisabled("Macro #%u", slot.id);
ImGui::Separator();
if (ImGui::MenuItem("Execute")) {
chatPanel.executeMacroText(gameHandler, inventoryScreen, spellbookScreen, questLogScreen, gameHandler.getMacroText(slot.id));
chatPanel.executeMacroText(gameHandler, gameHandler.getMacroText(slot.id));
}
if (ImGui::MenuItem("Edit")) {
const std::string& txt = gameHandler.getMacroText(slot.id);

View file

@ -0,0 +1,128 @@
// ChatBubbleManager — 3D-projected chat bubbles above entities.
// Moved from ChatPanel::renderBubbles / setupCallbacks (Phase 1.4).
#include "ui/chat/chat_bubble_manager.hpp"
#include "game/game_handler.hpp"
#include "rendering/renderer.hpp"
#include "rendering/camera.hpp"
#include "core/coordinates.hpp"
#include "core/window.hpp"
#include <imgui.h>
#include <glm/glm.hpp>
namespace wowee { namespace ui {
void ChatBubbleManager::addBubble(uint64_t senderGuid, const std::string& message, bool isYell) {
float duration = 8.0f + static_cast<float>(message.size()) * 0.06f;
if (isYell) duration += 2.0f;
if (duration > 15.0f) duration = 15.0f;
// Replace existing bubble for same sender
for (auto& b : bubbles_) {
if (b.senderGuid == senderGuid) {
b.message = message;
b.timeRemaining = duration;
b.totalDuration = duration;
b.isYell = isYell;
return;
}
}
// Evict oldest if too many
if (bubbles_.size() >= kMaxBubbles) {
bubbles_.erase(bubbles_.begin());
}
bubbles_.push_back({senderGuid, message, duration, duration, isYell});
}
void ChatBubbleManager::render(game::GameHandler& gameHandler, const UIServices& services) {
if (bubbles_.empty()) return;
auto* renderer = services.renderer;
auto* camera = renderer ? renderer->getCamera() : nullptr;
if (!camera) return;
auto* window = services.window;
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
// Get delta time from ImGui
float dt = ImGui::GetIO().DeltaTime;
glm::mat4 viewProj = camera->getProjectionMatrix() * camera->getViewMatrix();
// Update and render bubbles
for (int i = static_cast<int>(bubbles_.size()) - 1; i >= 0; --i) {
auto& bubble = bubbles_[i];
bubble.timeRemaining -= dt;
if (bubble.timeRemaining <= 0.0f) {
bubbles_.erase(bubbles_.begin() + i);
continue;
}
// Get entity position
auto entity = gameHandler.getEntityManager().getEntity(bubble.senderGuid);
if (!entity) continue;
// Convert canonical → render coordinates, offset up by 2.5 units for bubble above head
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ() + 2.5f);
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
// Project to screen
glm::vec4 clipPos = viewProj * glm::vec4(renderPos, 1.0f);
if (clipPos.w <= 0.0f) continue; // Behind camera
glm::vec2 ndc(clipPos.x / clipPos.w, clipPos.y / clipPos.w);
float screenX = (ndc.x * 0.5f + 0.5f) * screenW;
// Camera bakes the Vulkan Y-flip into the projection matrix:
// NDC y=-1 is top, y=1 is bottom — same convention as nameplate/minimap projection.
float screenY = (ndc.y * 0.5f + 0.5f) * screenH;
// Skip if off-screen
if (screenX < -200.0f || screenX > screenW + 200.0f ||
screenY < -100.0f || screenY > screenH + 100.0f) continue;
// Fade alpha over last 2 seconds
float alpha = 1.0f;
if (bubble.timeRemaining < 2.0f) {
alpha = bubble.timeRemaining / 2.0f;
}
// Draw bubble window
std::string winId = "##ChatBubble" + std::to_string(bubble.senderGuid);
ImGui::SetNextWindowPos(ImVec2(screenX, screenY), ImGuiCond_Always, ImVec2(0.5f, 1.0f));
ImGui::SetNextWindowBgAlpha(0.7f * alpha);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoInputs |
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav;
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 4));
ImGui::Begin(winId.c_str(), nullptr, flags);
ImVec4 textColor = bubble.isYell
? ImVec4(1.0f, 0.2f, 0.2f, alpha)
: ImVec4(1.0f, 1.0f, 1.0f, alpha);
ImGui::PushStyleColor(ImGuiCol_Text, textColor);
ImGui::PushTextWrapPos(200.0f);
ImGui::TextWrapped("%s", bubble.message.c_str());
ImGui::PopTextWrapPos();
ImGui::PopStyleColor();
ImGui::End();
ImGui::PopStyleVar(2);
}
}
void ChatBubbleManager::setupCallback(game::GameHandler& gameHandler) {
if (!callbackSet_) {
gameHandler.setChatBubbleCallback([this](uint64_t guid, const std::string& msg, bool isYell) {
addBubble(guid, msg, isYell);
});
callbackSet_ = true;
}
}
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,55 @@
// ChatCommandRegistry — command registration + dispatch.
// Moved from the if/else chain in ChatPanel::sendChatMessage() (Phase 3.1).
#include "ui/chat/chat_command_registry.hpp"
#include <algorithm>
namespace wowee { namespace ui {
void ChatCommandRegistry::registerCommand(std::unique_ptr<IChatCommand> cmd) {
IChatCommand* raw = cmd.get();
for (const auto& alias : raw->aliases()) {
commandMap_[alias] = raw;
}
commands_.push_back(std::move(cmd));
}
ChatCommandResult ChatCommandRegistry::dispatch(const std::string& cmdLower,
ChatCommandContext& ctx) {
auto it = commandMap_.find(cmdLower);
if (it == commandMap_.end()) {
return {false, false}; // not handled
}
return it->second->execute(ctx);
}
std::vector<std::string> ChatCommandRegistry::getCompletions(const std::string& prefix) const {
std::vector<std::string> results;
for (const auto& [alias, cmd] : commandMap_) {
if (alias.size() >= prefix.size() &&
alias.compare(0, prefix.size(), prefix) == 0) {
results.push_back(alias);
}
}
std::sort(results.begin(), results.end());
return results;
}
std::vector<std::pair<std::string, std::string>> ChatCommandRegistry::getHelpEntries() const {
std::vector<std::pair<std::string, std::string>> entries;
for (const auto& cmd : commands_) {
const auto& aliases = cmd->aliases();
std::string helpText = cmd->helpText();
if (!aliases.empty() && !helpText.empty()) {
entries.emplace_back("/" + aliases[0], helpText);
}
}
std::sort(entries.begin(), entries.end());
return entries;
}
bool ChatCommandRegistry::hasCommand(const std::string& alias) const {
return commandMap_.find(alias) != commandMap_.end();
}
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,68 @@
#include "ui/chat/chat_input.hpp"
namespace wowee { namespace ui {
// Push a message to sent-history (skip pure whitespace, cap at kMaxHistory).
void ChatInput::pushToHistory(const std::string& msg) {
bool allSpace = true;
for (char c : msg) {
if (!std::isspace(static_cast<unsigned char>(c))) { allSpace = false; break; }
}
if (allSpace) return;
// Remove duplicate of last entry if identical
if (sentHistory_.empty() || sentHistory_.back() != msg) {
sentHistory_.push_back(msg);
if (static_cast<int>(sentHistory_.size()) > kMaxHistory)
sentHistory_.erase(sentHistory_.begin());
}
historyIdx_ = -1; // reset browsing position after send
}
// Navigate up through sent-history. Returns the entry or "" if at start.
std::string ChatInput::historyUp() {
const int histSize = static_cast<int>(sentHistory_.size());
if (histSize == 0) return "";
if (historyIdx_ == -1)
historyIdx_ = histSize - 1;
else if (historyIdx_ > 0)
--historyIdx_;
return sentHistory_[historyIdx_];
}
// Navigate down through sent-history. Returns the entry or "" if past end.
std::string ChatInput::historyDown() {
const int histSize = static_cast<int>(sentHistory_.size());
if (histSize == 0 || historyIdx_ == -1) return "";
++historyIdx_;
if (historyIdx_ >= histSize) {
historyIdx_ = -1;
return "";
}
return sentHistory_[historyIdx_];
}
// Insert a spell / item link into the chat input buffer (shift-click).
void ChatInput::insertLink(const std::string& link) {
if (link.empty()) return;
size_t curLen = std::strlen(buffer_);
if (curLen + link.size() + 1 < sizeof(buffer_)) {
strncat(buffer_, link.c_str(), sizeof(buffer_) - curLen - 1);
moveCursorToEnd_ = true;
focusRequested_ = true;
}
}
// Activate the input field with a leading '/' (slash key).
void ChatInput::activateSlashInput() {
focusRequested_ = true;
buffer_[0] = '/';
buffer_[1] = '\0';
moveCursorToEnd_ = true;
}
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,206 @@
// ChatMarkupParser — parse WoW markup into typed segments.
// Moved from inline lambdas in ChatPanel::render() (Phase 2.1).
#include "ui/chat/chat_markup_parser.hpp"
#include <algorithm>
#include <cstdlib>
namespace wowee { namespace ui {
ImVec4 ChatMarkupParser::parseWowColor(const std::string& text, size_t pos) {
// |cAARRGGBB (10 chars total: |c + 8 hex)
if (pos + 10 > text.size()) return ImVec4(1, 1, 1, 1);
auto hexByte = [&](size_t offset) -> float {
const char* s = text.c_str() + pos + offset;
char buf[3] = {s[0], s[1], '\0'};
return static_cast<float>(strtol(buf, nullptr, 16)) / 255.0f;
};
float a = hexByte(2);
float r = hexByte(4);
float g = hexByte(6);
float b = hexByte(8);
return ImVec4(r, g, b, a);
}
std::vector<ChatSegment> ChatMarkupParser::parse(const std::string& text) const {
std::vector<ChatSegment> segments;
size_t pos = 0;
while (pos < text.size()) {
// Find next special element: URL or WoW link
size_t urlStart = text.find("https://", pos);
// Find next WoW link (may be colored with |c prefix or bare |H)
size_t linkStart = text.find("|c", pos);
// Also handle bare |H links without color prefix
size_t bareItem = text.find("|Hitem:", pos);
size_t bareSpell = text.find("|Hspell:", pos);
size_t bareQuest = text.find("|Hquest:", pos);
size_t bareLinkStart = std::min({bareItem, bareSpell, bareQuest});
// Determine which comes first
size_t nextSpecial = std::min({urlStart, linkStart, bareLinkStart});
if (nextSpecial == std::string::npos) {
// No more special elements — remainder is plain text
std::string remaining = text.substr(pos);
if (!remaining.empty()) {
segments.push_back({SegmentType::Text, std::move(remaining)});
}
break;
}
// Emit plain text before the special element
if (nextSpecial > pos) {
segments.push_back({SegmentType::Text, text.substr(pos, nextSpecial - pos)});
}
// Handle WoW link (|c... or bare |H...)
if (nextSpecial == linkStart || nextSpecial == bareLinkStart) {
ImVec4 linkColor(1, 1, 1, 1);
size_t hStart = std::string::npos;
if (nextSpecial == linkStart && text.size() > linkStart + 10) {
// Parse |cAARRGGBB color
linkColor = parseWowColor(text, linkStart);
// Find the nearest |H link of any supported type
size_t hItem = text.find("|Hitem:", linkStart + 10);
size_t hSpell = text.find("|Hspell:", linkStart + 10);
size_t hQuest = text.find("|Hquest:", linkStart + 10);
size_t hAch = text.find("|Hachievement:", linkStart + 10);
hStart = std::min({hItem, hSpell, hQuest, hAch});
} else if (nextSpecial == bareLinkStart) {
hStart = bareLinkStart;
}
if (hStart != std::string::npos) {
// Determine link type
const bool isSpellLink = (text.compare(hStart, 8, "|Hspell:") == 0);
const bool isQuestLink = (text.compare(hStart, 8, "|Hquest:") == 0);
const bool isAchievLink = (text.compare(hStart, 14, "|Hachievement:") == 0);
// Parse the first numeric ID after |Htype:
size_t idOffset = isSpellLink ? 8 : (isQuestLink ? 8 : (isAchievLink ? 14 : 7));
size_t entryStart = hStart + idOffset;
size_t entryEnd = text.find(':', entryStart);
uint32_t linkId = 0;
if (entryEnd != std::string::npos) {
linkId = static_cast<uint32_t>(strtoul(
text.substr(entryStart, entryEnd - entryStart).c_str(), nullptr, 10));
}
// Parse quest level (second field after questId)
uint32_t questLevel = 0;
if (isQuestLink && entryEnd != std::string::npos) {
size_t lvlEnd = text.find(':', entryEnd + 1);
if (lvlEnd == std::string::npos) lvlEnd = text.find('|', entryEnd + 1);
if (lvlEnd != std::string::npos) {
questLevel = static_cast<uint32_t>(strtoul(
text.substr(entryEnd + 1, lvlEnd - entryEnd - 1).c_str(), nullptr, 10));
}
}
// Find display name: |h[Name]|h
size_t nameTagStart = text.find("|h[", hStart);
size_t nameTagEnd = (nameTagStart != std::string::npos)
? text.find("]|h", nameTagStart + 3) : std::string::npos;
std::string linkName = isSpellLink ? "Unknown Spell"
: isQuestLink ? "Unknown Quest"
: isAchievLink ? "Unknown Achievement"
: "Unknown Item";
if (nameTagStart != std::string::npos && nameTagEnd != std::string::npos) {
linkName = text.substr(nameTagStart + 3, nameTagEnd - nameTagStart - 3);
}
// Find end of entire link sequence (|r or after ]|h)
size_t linkEnd = (nameTagEnd != std::string::npos) ? nameTagEnd + 3 : hStart + idOffset;
size_t resetPos = text.find("|r", linkEnd);
if (resetPos != std::string::npos && resetPos <= linkEnd + 2) {
linkEnd = resetPos + 2;
}
// Build raw link text for shift-click re-insertion
std::string rawLink = text.substr(nextSpecial, linkEnd - nextSpecial);
// Emit appropriate segment type
SegmentType stype = isSpellLink ? SegmentType::SpellLink
: isQuestLink ? SegmentType::QuestLink
: isAchievLink ? SegmentType::AchievementLink
: SegmentType::ItemLink;
ChatSegment seg;
seg.type = stype;
seg.text = std::move(linkName);
seg.color = linkColor;
seg.id = linkId;
seg.extra = questLevel;
seg.rawLink = std::move(rawLink);
segments.push_back(std::move(seg));
pos = linkEnd;
continue;
}
// Not an item link — treat as colored text: |cAARRGGBB...text...|r
if (nextSpecial == linkStart && text.size() > linkStart + 10) {
ImVec4 cColor = parseWowColor(text, linkStart);
size_t textStart = linkStart + 10; // after |cAARRGGBB
size_t resetPos2 = text.find("|r", textStart);
std::string coloredText;
if (resetPos2 != std::string::npos) {
coloredText = text.substr(textStart, resetPos2 - textStart);
pos = resetPos2 + 2; // skip |r
} else {
coloredText = text.substr(textStart);
pos = text.size();
}
// Strip any remaining WoW markup from the colored segment
// (e.g. |H...|h pairs that aren't item links)
std::string clean;
for (size_t i = 0; i < coloredText.size(); i++) {
if (coloredText[i] == '|' && i + 1 < coloredText.size()) {
char next = coloredText[i + 1];
if (next == 'H') {
// Skip |H...|h
size_t hEnd = coloredText.find("|h", i + 2);
if (hEnd != std::string::npos) { i = hEnd + 1; continue; }
} else if (next == 'h') {
i += 1; continue; // skip |h
} else if (next == 'r') {
i += 1; continue; // skip |r
}
}
clean += coloredText[i];
}
if (!clean.empty()) {
ChatSegment seg;
seg.type = SegmentType::ColoredText;
seg.text = std::move(clean);
seg.color = cColor;
segments.push_back(std::move(seg));
}
} else {
// Bare |c without enough chars for color — render literally
segments.push_back({SegmentType::Text, "|c"});
pos = nextSpecial + 2;
}
continue;
}
// Handle URL
if (nextSpecial == urlStart) {
size_t urlEnd = text.find_first_of(" \t\n\r", urlStart);
if (urlEnd == std::string::npos) urlEnd = text.size();
std::string url = text.substr(urlStart, urlEnd - urlStart);
segments.push_back({SegmentType::Url, std::move(url)});
pos = urlEnd;
continue;
}
}
return segments;
}
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,704 @@
// ChatMarkupRenderer — render parsed ChatSegments via ImGui.
// Moved from ChatPanel::render() inline lambdas (Phase 2.2).
#include "ui/chat/chat_markup_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(
const std::vector<ChatSegment>& segments,
const ImVec4& baseColor,
const MarkupRenderContext& ctx) const
{
for (size_t i = 0; i < segments.size(); ++i) {
const auto& seg = segments[i];
bool needSameLine = (i + 1 < segments.size());
switch (seg.type) {
case SegmentType::Text: {
if (!seg.text.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, baseColor);
ImGui::TextWrapped("%s", seg.text.c_str());
ImGui::PopStyleColor();
if (needSameLine) ImGui::SameLine(0, 0);
}
break;
}
case SegmentType::ColoredText: {
if (!seg.text.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, seg.color);
ImGui::TextWrapped("%s", seg.text.c_str());
ImGui::PopStyleColor();
if (needSameLine) ImGui::SameLine(0, 0);
}
break;
}
case SegmentType::ItemLink: {
uint32_t itemEntry = seg.id;
if (itemEntry > 0 && ctx.gameHandler) {
ctx.gameHandler->ensureItemInfo(itemEntry);
}
// Show small icon before item link if available
if (itemEntry > 0 && ctx.gameHandler && ctx.inventory) {
const auto* chatInfo = ctx.gameHandler->getItemInfo(itemEntry);
if (chatInfo && chatInfo->valid && chatInfo->displayInfoId != 0) {
VkDescriptorSet chatIcon = ctx.inventory->getItemIcon(chatInfo->displayInfoId);
if (chatIcon) {
ImGui::Image((ImTextureID)(uintptr_t)chatIcon, ImVec2(12, 12));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
renderItemTooltip(itemEntry, *ctx.gameHandler, *ctx.inventory, ctx.assetMgr);
}
ImGui::SameLine(0, 2);
}
}
}
std::string display = "[" + seg.text + "]";
ImGui::PushStyleColor(ImGuiCol_Text, seg.color);
ImGui::TextWrapped("%s", display.c_str());
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (itemEntry > 0 && ctx.gameHandler && ctx.inventory) {
renderItemTooltip(itemEntry, *ctx.gameHandler, *ctx.inventory, ctx.assetMgr);
}
}
// Shift-click: insert entire link back into chat input
if (ImGui::IsItemClicked() && ImGui::GetIO().KeyShift &&
ctx.chatInputBuffer && ctx.moveCursorToEnd) {
size_t curLen = strlen(ctx.chatInputBuffer);
if (curLen + seg.rawLink.size() + 1 < ctx.chatInputBufSize) {
strncat(ctx.chatInputBuffer, seg.rawLink.c_str(), ctx.chatInputBufSize - curLen - 1);
*ctx.moveCursorToEnd = true;
}
}
if (needSameLine) ImGui::SameLine(0, 0);
break;
}
case SegmentType::SpellLink: {
// Small icon (use spell icon cache if available)
VkDescriptorSet spellIcon = VK_NULL_HANDLE;
if (seg.id > 0 && ctx.getSpellIcon && ctx.assetMgr) {
spellIcon = ctx.getSpellIcon(seg.id, ctx.assetMgr);
}
if (spellIcon) {
ImGui::Image((ImTextureID)(uintptr_t)spellIcon, ImVec2(12, 12));
if (ImGui::IsItemHovered() && ctx.spellbook && ctx.gameHandler && ctx.assetMgr) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ctx.spellbook->renderSpellInfoTooltip(seg.id, *ctx.gameHandler, ctx.assetMgr);
}
ImGui::SameLine(0, 2);
}
std::string display = "[" + seg.text + "]";
ImGui::PushStyleColor(ImGuiCol_Text, seg.color);
ImGui::TextWrapped("%s", display.c_str());
ImGui::PopStyleColor();
if (ImGui::IsItemHovered() && ctx.spellbook && ctx.gameHandler && ctx.assetMgr) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ctx.spellbook->renderSpellInfoTooltip(seg.id, *ctx.gameHandler, ctx.assetMgr);
}
// Shift-click: insert link
if (ImGui::IsItemClicked() && ImGui::GetIO().KeyShift &&
ctx.chatInputBuffer && ctx.moveCursorToEnd) {
size_t curLen = strlen(ctx.chatInputBuffer);
if (curLen + seg.rawLink.size() + 1 < ctx.chatInputBufSize) {
strncat(ctx.chatInputBuffer, seg.rawLink.c_str(), ctx.chatInputBufSize - curLen - 1);
*ctx.moveCursorToEnd = true;
}
}
if (needSameLine) ImGui::SameLine(0, 0);
break;
}
case SegmentType::QuestLink: {
std::string display = "[" + seg.text + "]";
ImGui::PushStyleColor(ImGuiCol_Text, colors::kWarmGold);
ImGui::TextWrapped("%s", display.c_str());
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::BeginTooltip();
ImGui::TextColored(colors::kWarmGold, "%s", seg.text.c_str());
if (seg.extra > 0) ImGui::TextDisabled("Level %u Quest", seg.extra);
ImGui::TextDisabled("Click quest log to view details");
ImGui::EndTooltip();
}
if (ImGui::IsItemClicked() && seg.id > 0 && ctx.questLog) {
ctx.questLog->openAndSelectQuest(seg.id);
}
// Shift-click: insert link
if (ImGui::IsItemClicked() && ImGui::GetIO().KeyShift &&
ctx.chatInputBuffer && ctx.moveCursorToEnd) {
size_t curLen = strlen(ctx.chatInputBuffer);
if (curLen + seg.rawLink.size() + 1 < ctx.chatInputBufSize) {
strncat(ctx.chatInputBuffer, seg.rawLink.c_str(), ctx.chatInputBufSize - curLen - 1);
*ctx.moveCursorToEnd = true;
}
}
if (needSameLine) ImGui::SameLine(0, 0);
break;
}
case SegmentType::AchievementLink: {
std::string display = "[" + seg.text + "]";
ImGui::PushStyleColor(ImGuiCol_Text, colors::kBrightGold);
ImGui::TextWrapped("%s", display.c_str());
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Achievement: %s", seg.text.c_str());
}
// Shift-click: insert link
if (ImGui::IsItemClicked() && ImGui::GetIO().KeyShift &&
ctx.chatInputBuffer && ctx.moveCursorToEnd) {
size_t curLen = strlen(ctx.chatInputBuffer);
if (curLen + seg.rawLink.size() + 1 < ctx.chatInputBufSize) {
strncat(ctx.chatInputBuffer, seg.rawLink.c_str(), ctx.chatInputBufSize - curLen - 1);
*ctx.moveCursorToEnd = true;
}
}
if (needSameLine) ImGui::SameLine(0, 0);
break;
}
case SegmentType::Url: {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.7f, 1.0f, 1.0f));
ImGui::TextWrapped("%s", seg.text.c_str());
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Open: %s", seg.text.c_str());
}
if (ImGui::IsItemClicked()) {
std::string cmd = "xdg-open '" + seg.text + "' &";
[[maybe_unused]] int result = system(cmd.c_str());
}
ImGui::PopStyleColor();
if (needSameLine) ImGui::SameLine(0, 0);
break;
}
} // switch
}
}
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,63 @@
#include "ui/chat/chat_settings.hpp"
#include <imgui.h>
namespace wowee { namespace ui {
// Reset all chat settings to defaults.
void ChatSettings::restoreDefaults() {
showTimestamps = false;
fontSize = 1;
autoJoinGeneral = true;
autoJoinTrade = true;
autoJoinLocalDefense = true;
autoJoinLFG = true;
autoJoinLocal = true;
}
// Render the "Chat" tab inside the Settings window.
void ChatSettings::renderSettingsTab(std::function<void()> saveSettingsFn) {
ImGui::Spacing();
ImGui::Text("Appearance");
ImGui::Separator();
if (ImGui::Checkbox("Show Timestamps", &showTimestamps)) {
saveSettingsFn();
}
ImGui::SetItemTooltip("Show [HH:MM] before each chat message");
const char* fontSizes[] = { "Small", "Medium", "Large" };
if (ImGui::Combo("Chat Font Size", &fontSize, fontSizes, 3)) {
saveSettingsFn();
}
ImGui::Spacing();
ImGui::Spacing();
ImGui::Text("Auto-Join Channels");
ImGui::Separator();
if (ImGui::Checkbox("General", &autoJoinGeneral)) saveSettingsFn();
if (ImGui::Checkbox("Trade", &autoJoinTrade)) saveSettingsFn();
if (ImGui::Checkbox("LocalDefense", &autoJoinLocalDefense)) saveSettingsFn();
if (ImGui::Checkbox("LookingForGroup", &autoJoinLFG)) saveSettingsFn();
if (ImGui::Checkbox("Local", &autoJoinLocal)) saveSettingsFn();
ImGui::Spacing();
ImGui::Spacing();
ImGui::Text("Joined Channels");
ImGui::Separator();
ImGui::TextDisabled("Use /join and /leave commands in chat to manage channels.");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
if (ImGui::Button("Restore Chat Defaults", ImVec2(-1, 0))) {
restoreDefaults();
saveSettingsFn();
}
}
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,35 @@
// ChatTabCompleter — cycling tab-completion state machine.
// Extracted from ChatPanel tab-completion logic (Phase 5.1).
#include "ui/chat/chat_tab_completer.hpp"
namespace wowee { namespace ui {
void ChatTabCompleter::startCompletion(const std::string& prefix,
std::vector<std::string> candidates) {
prefix_ = prefix;
matches_ = std::move(candidates);
matchIdx_ = matches_.empty() ? -1 : 0;
}
bool ChatTabCompleter::next() {
if (matches_.empty()) return false;
++matchIdx_;
if (matchIdx_ >= static_cast<int>(matches_.size()))
matchIdx_ = 0;
return true;
}
std::string ChatTabCompleter::getCurrentMatch() const {
if (matchIdx_ < 0 || matchIdx_ >= static_cast<int>(matches_.size()))
return "";
return matches_[matchIdx_];
}
void ChatTabCompleter::reset() {
prefix_.clear();
matches_.clear();
matchIdx_ = -1;
}
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,198 @@
#include "ui/chat/chat_tab_manager.hpp"
#include "ui/ui_colors.hpp"
#include <algorithm>
namespace {
using namespace wowee::ui::colors;
} // namespace
namespace wowee { namespace ui {
ChatTabManager::ChatTabManager() {
initTabs();
}
void ChatTabManager::initTabs() {
tabs_.clear();
// General tab: shows everything
tabs_.push_back({"General", ~0ULL});
// Combat tab: system, loot, skills, achievements, and NPC speech/emotes
tabs_.push_back({"Combat", (1ULL << static_cast<uint8_t>(game::ChatType::SYSTEM)) |
(1ULL << static_cast<uint8_t>(game::ChatType::LOOT)) |
(1ULL << static_cast<uint8_t>(game::ChatType::SKILL)) |
(1ULL << static_cast<uint8_t>(game::ChatType::ACHIEVEMENT)) |
(1ULL << static_cast<uint8_t>(game::ChatType::GUILD_ACHIEVEMENT)) |
(1ULL << static_cast<uint8_t>(game::ChatType::MONSTER_SAY)) |
(1ULL << static_cast<uint8_t>(game::ChatType::MONSTER_YELL)) |
(1ULL << static_cast<uint8_t>(game::ChatType::MONSTER_EMOTE)) |
(1ULL << static_cast<uint8_t>(game::ChatType::MONSTER_WHISPER)) |
(1ULL << static_cast<uint8_t>(game::ChatType::MONSTER_PARTY)) |
(1ULL << static_cast<uint8_t>(game::ChatType::RAID_BOSS_WHISPER)) |
(1ULL << static_cast<uint8_t>(game::ChatType::RAID_BOSS_EMOTE))});
// Whispers tab
tabs_.push_back({"Whispers", (1ULL << static_cast<uint8_t>(game::ChatType::WHISPER)) |
(1ULL << static_cast<uint8_t>(game::ChatType::WHISPER_INFORM))});
// Guild tab: guild and officer chat
tabs_.push_back({"Guild", (1ULL << static_cast<uint8_t>(game::ChatType::GUILD)) |
(1ULL << static_cast<uint8_t>(game::ChatType::OFFICER)) |
(1ULL << static_cast<uint8_t>(game::ChatType::GUILD_ACHIEVEMENT))});
// Trade/LFG tab: channel messages
tabs_.push_back({"Trade/LFG", (1ULL << static_cast<uint8_t>(game::ChatType::CHANNEL))});
unread_.assign(tabs_.size(), 0);
seenCount_ = 0;
}
int ChatTabManager::getUnreadCount(int idx) const {
if (idx < 0 || idx >= static_cast<int>(unread_.size())) return 0;
return unread_[idx];
}
void ChatTabManager::clearUnread(int idx) {
if (idx >= 0 && idx < static_cast<int>(unread_.size()))
unread_[idx] = 0;
}
void ChatTabManager::updateUnread(const std::deque<game::MessageChatData>& history, int activeTab) {
// Ensure unread array is sized correctly (guards against late init)
if (unread_.size() != tabs_.size())
unread_.assign(tabs_.size(), 0);
// If history shrank (e.g. cleared), reset
if (seenCount_ > history.size()) seenCount_ = 0;
for (size_t mi = seenCount_; mi < history.size(); ++mi) {
const auto& msg = history[mi];
// For each non-General (non-0) tab that isn't currently active, check visibility
for (int ti = 1; ti < static_cast<int>(tabs_.size()); ++ti) {
if (ti == activeTab) continue;
if (shouldShowMessage(msg, ti)) {
unread_[ti]++;
}
}
}
seenCount_ = history.size();
}
bool ChatTabManager::shouldShowMessage(const game::MessageChatData& msg, int tabIndex) const {
if (tabIndex < 0 || tabIndex >= static_cast<int>(tabs_.size())) return true;
const auto& tab = tabs_[tabIndex];
if (tab.typeMask == ~0ULL) return true; // General tab shows all
uint64_t typeBit = 1ULL << static_cast<uint8_t>(msg.type);
// For Trade/LFG tab (index 4), also filter by channel name
if (tabIndex == 4 && msg.type == game::ChatType::CHANNEL) {
const std::string& ch = msg.channelName;
if (ch.find("Trade") == std::string::npos &&
ch.find("General") == std::string::npos &&
ch.find("LookingForGroup") == std::string::npos &&
ch.find("Local") == std::string::npos) {
return false;
}
return true;
}
return (tab.typeMask & typeBit) != 0;
}
// ---- Static chat type helpers ----
const char* ChatTabManager::getChatTypeName(game::ChatType type) {
switch (type) {
case game::ChatType::SAY: return "Say";
case game::ChatType::YELL: return "Yell";
case game::ChatType::EMOTE: return "Emote";
case game::ChatType::TEXT_EMOTE: return "Emote";
case game::ChatType::PARTY: return "Party";
case game::ChatType::GUILD: return "Guild";
case game::ChatType::OFFICER: return "Officer";
case game::ChatType::RAID: return "Raid";
case game::ChatType::RAID_LEADER: return "Raid Leader";
case game::ChatType::RAID_WARNING: return "Raid Warning";
case game::ChatType::BATTLEGROUND: return "Battleground";
case game::ChatType::BATTLEGROUND_LEADER: return "Battleground Leader";
case game::ChatType::WHISPER: return "Whisper";
case game::ChatType::WHISPER_INFORM: return "To";
case game::ChatType::SYSTEM: return "System";
case game::ChatType::MONSTER_SAY: return "Say";
case game::ChatType::MONSTER_YELL: return "Yell";
case game::ChatType::MONSTER_EMOTE: return "Emote";
case game::ChatType::CHANNEL: return "Channel";
case game::ChatType::ACHIEVEMENT: return "Achievement";
case game::ChatType::DND: return "DND";
case game::ChatType::AFK: return "AFK";
case game::ChatType::BG_SYSTEM_NEUTRAL:
case game::ChatType::BG_SYSTEM_ALLIANCE:
case game::ChatType::BG_SYSTEM_HORDE: return "System";
default: return "Unknown";
}
}
ImVec4 ChatTabManager::getChatTypeColor(game::ChatType type) {
switch (type) {
case game::ChatType::SAY:
return kWhite;
case game::ChatType::YELL:
return kRed;
case game::ChatType::EMOTE:
case game::ChatType::TEXT_EMOTE:
return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange
case game::ChatType::PARTY:
return ImVec4(0.5f, 0.5f, 1.0f, 1.0f); // Light blue
case game::ChatType::GUILD:
return kBrightGreen;
case game::ChatType::OFFICER:
return ImVec4(0.3f, 0.8f, 0.3f, 1.0f); // Dark green
case game::ChatType::RAID:
return ImVec4(1.0f, 0.5f, 0.0f, 1.0f); // Orange
case game::ChatType::RAID_LEADER:
return ImVec4(1.0f, 0.4f, 0.0f, 1.0f); // Darker orange
case game::ChatType::RAID_WARNING:
return ImVec4(1.0f, 0.0f, 0.0f, 1.0f); // Red
case game::ChatType::BATTLEGROUND:
return ImVec4(1.0f, 0.6f, 0.0f, 1.0f); // Orange-gold
case game::ChatType::BATTLEGROUND_LEADER:
return ImVec4(1.0f, 0.5f, 0.0f, 1.0f); // Orange
case game::ChatType::WHISPER:
case game::ChatType::WHISPER_INFORM:
return ImVec4(1.0f, 0.5f, 1.0f, 1.0f); // Pink
case game::ChatType::SYSTEM:
return kYellow;
case game::ChatType::MONSTER_SAY:
return kWhite;
case game::ChatType::MONSTER_YELL:
return kRed;
case game::ChatType::MONSTER_EMOTE:
return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange
case game::ChatType::CHANNEL:
return ImVec4(1.0f, 0.7f, 0.7f, 1.0f); // Light pink
case game::ChatType::ACHIEVEMENT:
return ImVec4(1.0f, 1.0f, 0.0f, 1.0f); // Bright yellow
case game::ChatType::GUILD_ACHIEVEMENT:
return kWarmGold;
case game::ChatType::SKILL:
return kCyan;
case game::ChatType::LOOT:
return ImVec4(0.8f, 0.5f, 1.0f, 1.0f); // Light purple
case game::ChatType::MONSTER_WHISPER:
case game::ChatType::RAID_BOSS_WHISPER:
return ImVec4(1.0f, 0.5f, 1.0f, 1.0f); // Pink
case game::ChatType::RAID_BOSS_EMOTE:
return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange
case game::ChatType::MONSTER_PARTY:
return ImVec4(0.5f, 0.5f, 1.0f, 1.0f); // Light blue
case game::ChatType::BG_SYSTEM_NEUTRAL:
return kWarmGold;
case game::ChatType::BG_SYSTEM_ALLIANCE:
return ImVec4(0.3f, 0.6f, 1.0f, 1.0f); // Blue
case game::ChatType::BG_SYSTEM_HORDE:
return kRed;
case game::ChatType::AFK:
case game::ChatType::DND:
return ImVec4(0.85f, 0.85f, 0.85f, 0.8f); // Light gray
default:
return kLightGray;
}
}
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,341 @@
// Channel commands: /s, /y, /p, /g, /raid, /rw, /o, /bg, /i, /join, /leave, /wts, /wtb, /1-9, /w, /r
// Moved from ChatPanel::sendChatMessage() channel dispatch section (Phase 3).
// These commands send messages to specific chat channels and/or switch the
// chat-type dropdown on the panel.
#include "ui/chat/i_chat_command.hpp"
#include "ui/chat_panel.hpp"
#include "ui/chat/chat_utils.hpp"
#include "game/game_handler.hpp"
#include <algorithm>
#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;
}
// Send a whisper, intercepting PortBot targets for GM teleport commands.
// Returns true if the whisper was handled (PortBot or normal send), false if empty.
bool sendWhisperOrPortBot(wowee::game::GameHandler& gameHandler,
const std::string& target,
const std::string& message) {
if (isPortBotTarget(target)) {
std::string cmd = buildPortBotCommand(message);
wowee::game::MessageChatData msg;
msg.type = wowee::game::ChatType::SYSTEM;
msg.language = wowee::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);
return true;
}
gameHandler.sendChatMessage(wowee::game::ChatType::SAY, cmd, "");
msg.message = "PortBot executed: " + cmd;
gameHandler.addLocalChatMessage(msg);
return true;
}
if (!message.empty()) {
gameHandler.sendChatMessage(wowee::game::ChatType::WHISPER, message, target);
}
return true;
}
} // anonymous namespace
namespace wowee { namespace ui {
// --- Helper: send a message via a specific chat type + optionally switch dropdown ---
static ChatCommandResult sendAndSwitch(ChatCommandContext& ctx,
game::ChatType chatType,
int switchIdx,
const std::string& target = "") {
if (!ctx.args.empty())
ctx.gameHandler.sendChatMessage(chatType, ctx.args, target);
ctx.panel.setSelectedChatType(switchIdx);
return {};
}
// --- /s, /say ---
class SayCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
return sendAndSwitch(ctx, game::ChatType::SAY, 0);
}
std::vector<std::string> aliases() const override { return {"s", "say"}; }
std::string helpText() const override { return "Say to nearby players"; }
};
// --- /y, /yell, /shout ---
class YellCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
return sendAndSwitch(ctx, game::ChatType::YELL, 1);
}
std::vector<std::string> aliases() const override { return {"y", "yell", "shout"}; }
std::string helpText() const override { return "Yell to a wider area"; }
};
// --- /p, /party ---
class PartyCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
return sendAndSwitch(ctx, game::ChatType::PARTY, 2);
}
std::vector<std::string> aliases() const override { return {"p", "party"}; }
std::string helpText() const override { return "Party chat"; }
};
// --- /g, /guild ---
class GuildChatCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
return sendAndSwitch(ctx, game::ChatType::GUILD, 3);
}
std::vector<std::string> aliases() const override { return {"g", "guild"}; }
std::string helpText() const override { return "Guild chat"; }
};
// --- /raid, /rsay, /ra ---
class RaidChatCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
return sendAndSwitch(ctx, game::ChatType::RAID, 5);
}
std::vector<std::string> aliases() const override { return {"raid", "rsay", "ra"}; }
std::string helpText() const override { return "Raid chat"; }
};
// --- /raidwarning, /rw ---
class RaidWarningCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
return sendAndSwitch(ctx, game::ChatType::RAID_WARNING, 8);
}
std::vector<std::string> aliases() const override { return {"raidwarning", "rw"}; }
std::string helpText() const override { return "Raid warning"; }
};
// --- /officer, /o, /osay ---
class OfficerCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
return sendAndSwitch(ctx, game::ChatType::OFFICER, 6);
}
std::vector<std::string> aliases() const override { return {"officer", "o", "osay"}; }
std::string helpText() const override { return "Guild officer chat"; }
};
// --- /battleground, /bg ---
class BattlegroundChatCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
return sendAndSwitch(ctx, game::ChatType::BATTLEGROUND, 7);
}
std::vector<std::string> aliases() const override { return {"battleground", "bg"}; }
std::string helpText() const override { return "Battleground chat"; }
};
// --- /instance, /i ---
class InstanceChatCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
return sendAndSwitch(ctx, game::ChatType::PARTY, 9);
}
std::vector<std::string> aliases() const override { return {"instance", "i"}; }
std::string helpText() const override { return "Instance chat"; }
};
// --- /join ---
class JoinCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (ctx.args.empty() && ctx.gameHandler.hasPendingBgInvite()) {
ctx.gameHandler.acceptBattlefield();
return {};
}
if (!ctx.args.empty()) {
size_t pwStart = ctx.args.find(' ');
std::string channelName = (pwStart != std::string::npos) ? ctx.args.substr(0, pwStart) : ctx.args;
std::string password = (pwStart != std::string::npos) ? ctx.args.substr(pwStart + 1) : "";
ctx.gameHandler.joinChannel(channelName, password);
}
return {};
}
std::vector<std::string> aliases() const override { return {"join"}; }
std::string helpText() const override { return "Join a chat channel"; }
};
// --- /leave (channel) ---
// Note: /leave without args is handled by group_commands (leave party).
// This command only triggers with args (channel name).
class LeaveChannelCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (!ctx.args.empty()) {
ctx.gameHandler.leaveChannel(ctx.args);
}
// If no args, the group LeaveCommand will handle /leave (leave party)
// so we return not-handled to allow fallthrough
if (ctx.args.empty()) return {false, false};
return {};
}
std::vector<std::string> aliases() const override { return {"leavechannel"}; }
std::string helpText() const override { return "Leave a chat channel"; }
};
// --- /wts, /wtb ---
class TradeChannelCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (ctx.args.empty()) return {false, false};
const std::string tag = (ctx.fullCommand == "wts") ? "[WTS] " : "[WTB] ";
std::string tradeChan;
for (const auto& ch : ctx.gameHandler.getJoinedChannels()) {
std::string chLow = ch;
for (char& c : chLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (chLow.rfind("trade", 0) == 0) { tradeChan = ch; break; }
}
if (tradeChan.empty()) {
game::MessageChatData errMsg;
errMsg.type = game::ChatType::SYSTEM;
errMsg.language = game::ChatLanguage::UNIVERSAL;
errMsg.message = "You are not in the Trade channel.";
ctx.gameHandler.addLocalChatMessage(errMsg);
return {};
}
ctx.gameHandler.sendChatMessage(game::ChatType::CHANNEL, tag + ctx.args, tradeChan);
return {};
}
std::vector<std::string> aliases() const override { return {"wts", "wtb"}; }
std::string helpText() const override { return "Send to Trade channel ([WTS]/[WTB] prefix)"; }
};
// --- /1 through /9 — channel shortcuts ---
class ChannelNumberCommand : public IChatCommand {
public:
explicit ChannelNumberCommand(int num) : num_(num), alias_(std::to_string(num)) {}
ChatCommandResult execute(ChatCommandContext& ctx) override {
std::string channelName = ctx.gameHandler.getChannelByIndex(num_);
if (channelName.empty()) {
game::MessageChatData errMsg;
errMsg.type = game::ChatType::SYSTEM;
errMsg.message = "You are not in channel " + std::to_string(num_) + ".";
ctx.gameHandler.addLocalChatMessage(errMsg);
return {};
}
if (!ctx.args.empty()) {
ctx.gameHandler.sendChatMessage(game::ChatType::CHANNEL, ctx.args, channelName);
}
return {};
}
std::vector<std::string> aliases() const override { return {alias_}; }
std::string helpText() const override { return "Send to channel " + alias_; }
private:
int num_;
std::string alias_;
};
// --- /w, /whisper, /tell, /t ---
class WhisperCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.panel.setSelectedChatType(4); // Switch to whisper mode
if (!ctx.args.empty()) {
size_t msgStart = ctx.args.find(' ');
if (msgStart != std::string::npos) {
// /w PlayerName message — send whisper immediately (PortBot-aware)
std::string target = ctx.args.substr(0, msgStart);
std::string message = ctx.args.substr(msgStart + 1);
sendWhisperOrPortBot(ctx.gameHandler, target, message);
// Set whisper target for future messages
char* buf = ctx.panel.getWhisperTargetBuffer();
size_t sz = ctx.panel.getWhisperTargetBufferSize();
strncpy(buf, target.c_str(), sz - 1);
buf[sz - 1] = '\0';
} else {
// /w PlayerName — switch to whisper mode with target set
char* buf = ctx.panel.getWhisperTargetBuffer();
size_t sz = ctx.panel.getWhisperTargetBufferSize();
strncpy(buf, ctx.args.c_str(), sz - 1);
buf[sz - 1] = '\0';
}
}
return {};
}
std::vector<std::string> aliases() const override { return {"w", "whisper", "tell", "t"}; }
std::string helpText() const override { return "Whisper to a player"; }
};
// --- /r, /reply ---
class ReplyCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.panel.setSelectedChatType(4);
std::string lastSender = ctx.gameHandler.getLastWhisperSender();
if (lastSender.empty()) {
game::MessageChatData sysMsg;
sysMsg.type = game::ChatType::SYSTEM;
sysMsg.language = game::ChatLanguage::UNIVERSAL;
sysMsg.message = "No one has whispered you yet.";
ctx.gameHandler.addLocalChatMessage(sysMsg);
return {};
}
char* buf = ctx.panel.getWhisperTargetBuffer();
size_t sz = ctx.panel.getWhisperTargetBufferSize();
strncpy(buf, lastSender.c_str(), sz - 1);
buf[sz - 1] = '\0';
if (!ctx.args.empty()) {
// PortBot-aware whisper send
sendWhisperOrPortBot(ctx.gameHandler, lastSender, ctx.args);
}
return {};
}
std::vector<std::string> aliases() const override { return {"r", "reply"}; }
std::string helpText() const override { return "Reply to last whisper"; }
};
// --- Registration ---
void registerChannelCommands(ChatCommandRegistry& reg) {
reg.registerCommand(std::make_unique<SayCommand>());
reg.registerCommand(std::make_unique<YellCommand>());
reg.registerCommand(std::make_unique<PartyCommand>());
reg.registerCommand(std::make_unique<GuildChatCommand>());
reg.registerCommand(std::make_unique<RaidChatCommand>());
reg.registerCommand(std::make_unique<RaidWarningCommand>());
reg.registerCommand(std::make_unique<OfficerCommand>());
reg.registerCommand(std::make_unique<BattlegroundChatCommand>());
reg.registerCommand(std::make_unique<InstanceChatCommand>());
reg.registerCommand(std::make_unique<JoinCommand>());
reg.registerCommand(std::make_unique<LeaveChannelCommand>());
reg.registerCommand(std::make_unique<TradeChannelCommand>());
for (int n = 1; n <= 9; ++n)
reg.registerCommand(std::make_unique<ChannelNumberCommand>(n));
reg.registerCommand(std::make_unique<WhisperCommand>());
reg.registerCommand(std::make_unique<ReplyCommand>());
}
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,543 @@
// Combat commands: /cast, /castsequence, /use, /equip, /equipset,
// /startattack, /stopattack, /stopcasting, /cancelqueuedspell
// Moved from ChatPanel::sendChatMessage() if/else chain (Phase 3).
#include "ui/chat/i_chat_command.hpp"
#include "ui/chat_panel.hpp"
#include "game/game_handler.hpp"
#include "game/inventory.hpp"
#include <imgui.h>
#include <algorithm>
#include <cctype>
#include <sstream>
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 {
// Trim leading/trailing whitespace from a string in-place.
inline void trimInPlace(std::string& s) {
while (!s.empty() && s.front() == ' ') s.erase(s.begin());
while (!s.empty() && s.back() == ' ') s.pop_back();
}
// Try to parse a spell by name among known spells, optionally with a specific
// rank. Returns the best matching spell ID, or 0 if none found.
uint32_t resolveSpellByName(game::GameHandler& gh, const std::string& spellArg, int requestedRank = -1) {
std::string spellName = spellArg;
// Parse optional "(Rank N)" suffix
{
auto rankPos = spellArg.find('(');
if (rankPos != std::string::npos) {
std::string rankStr = spellArg.substr(rankPos + 1);
auto closePos = rankStr.find(')');
if (closePos != std::string::npos) rankStr = rankStr.substr(0, closePos);
for (char& c : rankStr) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (rankStr.rfind("rank ", 0) == 0) {
try { requestedRank = std::stoi(rankStr.substr(5)); } catch (...) {}
}
spellName = spellArg.substr(0, rankPos);
while (!spellName.empty() && spellName.back() == ' ') spellName.pop_back();
}
}
std::string spellNameLower = spellName;
for (char& c : spellNameLower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
uint32_t bestId = 0;
int bestRank = -1;
for (uint32_t sid : gh.getKnownSpells()) {
const std::string& sName = gh.getSpellName(sid);
if (sName.empty()) continue;
std::string sLow = sName;
for (char& c : sLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (sLow != spellNameLower) continue;
int sRank = 0;
const std::string& rk = gh.getSpellRank(sid);
if (!rk.empty()) {
std::string rLow = rk;
for (char& c : rLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (rLow.rfind("rank ", 0) == 0) {
try { sRank = std::stoi(rLow.substr(5)); } catch (...) {}
}
}
if (requestedRank >= 0) {
if (sRank == requestedRank) return sid;
} else {
if (sRank > bestRank) { bestRank = sRank; bestId = sid; }
}
}
return bestId;
}
// Try to parse numeric spell ID (with optional '#' prefix). Returns 0 if not numeric.
uint32_t parseNumericSpellId(const std::string& str) {
std::string numStr = str;
if (!numStr.empty() && numStr.front() == '#') numStr.erase(numStr.begin());
bool isNumeric = !numStr.empty() &&
std::all_of(numStr.begin(), numStr.end(),
[](unsigned char c){ return std::isdigit(c); });
if (!isNumeric) return 0;
try { return static_cast<uint32_t>(std::stoul(numStr)); } catch (...) { return 0; }
}
} // anon namespace
// --- /cast ---
class CastCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (ctx.args.empty()) return {false, false};
std::string spellArg = ctx.args;
trimInPlace(spellArg);
// Evaluate WoW macro conditionals
uint64_t castTargetOverride = static_cast<uint64_t>(-1);
if (!spellArg.empty() && spellArg.front() == '[') {
spellArg = evaluateMacroConditionals(spellArg, ctx.gameHandler, castTargetOverride);
if (spellArg.empty()) return {}; // no conditional matched
trimInPlace(spellArg);
}
// Strip leading '!' (force recast)
if (!spellArg.empty() && spellArg.front() == '!') spellArg.erase(spellArg.begin());
// Numeric spell ID
uint32_t numId = parseNumericSpellId(spellArg);
if (numId) {
uint64_t targetGuid = (castTargetOverride != static_cast<uint64_t>(-1))
? castTargetOverride
: (ctx.gameHandler.hasTarget() ? ctx.gameHandler.getTargetGuid() : 0);
ctx.gameHandler.castSpell(numId, targetGuid);
return {};
}
// Name-based lookup
uint32_t bestSpellId = resolveSpellByName(ctx.gameHandler, spellArg);
if (bestSpellId) {
uint64_t targetGuid = (castTargetOverride != static_cast<uint64_t>(-1))
? castTargetOverride
: (ctx.gameHandler.hasTarget() ? ctx.gameHandler.getTargetGuid() : 0);
ctx.gameHandler.castSpell(bestSpellId, targetGuid);
} else {
// Build error message
std::string spellName = spellArg;
auto rp = spellArg.find('(');
if (rp != std::string::npos) {
spellName = spellArg.substr(0, rp);
while (!spellName.empty() && spellName.back() == ' ') spellName.pop_back();
}
// Check if a specific rank was requested for the error message
int reqRank = -1;
if (rp != std::string::npos) {
std::string rs = spellArg.substr(rp + 1);
auto cp = rs.find(')');
if (cp != std::string::npos) rs = rs.substr(0, cp);
for (char& c : rs) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (rs.rfind("rank ", 0) == 0) {
try { reqRank = std::stoi(rs.substr(5)); } catch (...) {}
}
}
game::MessageChatData sysMsg;
sysMsg.type = game::ChatType::SYSTEM;
sysMsg.language = game::ChatLanguage::UNIVERSAL;
sysMsg.message = (reqRank >= 0)
? "You don't know '" + spellName + "' (Rank " + std::to_string(reqRank) + ")."
: "Unknown spell: '" + spellName + "'.";
ctx.gameHandler.addLocalChatMessage(sysMsg);
}
return {};
}
std::vector<std::string> aliases() const override { return {"cast"}; }
std::string helpText() const override { return "Cast a spell by name or ID"; }
};
// --- /castsequence ---
class CastSequenceCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (ctx.args.empty()) return {false, false};
std::string seqArg = ctx.args;
trimInPlace(seqArg);
// Macro conditionals
uint64_t seqTgtOver = static_cast<uint64_t>(-1);
if (!seqArg.empty() && seqArg.front() == '[') {
seqArg = evaluateMacroConditionals(seqArg, ctx.gameHandler, seqTgtOver);
if (seqArg.empty() && seqTgtOver == static_cast<uint64_t>(-1)) return {};
trimInPlace(seqArg);
}
// Optional reset= spec
std::string resetSpec;
if (seqArg.rfind("reset=", 0) == 0) {
size_t spAfter = seqArg.find(' ');
if (spAfter != std::string::npos) {
resetSpec = seqArg.substr(6, spAfter - 6);
seqArg = seqArg.substr(spAfter + 1);
trimInPlace(seqArg);
}
}
// Parse comma-separated spell list
std::vector<std::string> seqSpells;
{
std::string cur;
for (char c : seqArg) {
if (c == ',') {
trimInPlace(cur);
if (!cur.empty()) seqSpells.push_back(cur);
cur.clear();
} else { cur += c; }
}
trimInPlace(cur);
if (!cur.empty()) seqSpells.push_back(cur);
}
if (seqSpells.empty()) return {};
// Build stable key from lowercase spell list
std::string seqKey;
for (size_t k = 0; k < seqSpells.size(); ++k) {
if (k) seqKey += ',';
std::string sl = seqSpells[k];
for (char& c : sl) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
seqKey += sl;
}
auto& seqState = ctx.panel.getCastSeqTracker().get(seqKey);
// Check reset conditions
float nowSec = static_cast<float>(ImGui::GetTime());
bool shouldReset = false;
if (!resetSpec.empty()) {
size_t rpos = 0;
while (rpos <= resetSpec.size()) {
size_t slash = resetSpec.find('/', rpos);
std::string part = (slash != std::string::npos)
? resetSpec.substr(rpos, slash - rpos)
: resetSpec.substr(rpos);
std::string plow = part;
for (char& c : plow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
bool isNum = !plow.empty() && std::all_of(plow.begin(), plow.end(),
[](unsigned char c){ return std::isdigit(c) || c == '.'; });
if (isNum) {
float rSec = 0.0f;
try { rSec = std::stof(plow); } catch (...) {}
if (rSec > 0.0f && nowSec - seqState.lastPressSec > rSec) shouldReset = true;
} else if (plow == "target") {
if (ctx.gameHandler.getTargetGuid() != seqState.lastTargetGuid) shouldReset = true;
} else if (plow == "combat") {
if (ctx.gameHandler.isInCombat() != seqState.lastInCombat) shouldReset = true;
}
if (slash == std::string::npos) break;
rpos = slash + 1;
}
}
if (shouldReset || seqState.index >= seqSpells.size()) seqState.index = 0;
const std::string& seqSpell = seqSpells[seqState.index];
seqState.index = (seqState.index + 1) % seqSpells.size();
seqState.lastPressSec = nowSec;
seqState.lastTargetGuid = ctx.gameHandler.getTargetGuid();
seqState.lastInCombat = ctx.gameHandler.isInCombat();
// Cast the selected spell
std::string ssLow = seqSpell;
for (char& c : ssLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (!ssLow.empty() && ssLow.front() == '!') ssLow.erase(ssLow.begin());
uint64_t seqTargetGuid = (seqTgtOver != static_cast<uint64_t>(-1) && seqTgtOver != 0)
? seqTgtOver : (ctx.gameHandler.hasTarget() ? ctx.gameHandler.getTargetGuid() : 0);
uint32_t numId = parseNumericSpellId(ssLow);
if (numId) {
ctx.gameHandler.castSpell(numId, seqTargetGuid);
} else {
uint32_t best = resolveSpellByName(ctx.gameHandler, ssLow);
if (best) ctx.gameHandler.castSpell(best, seqTargetGuid);
}
return {};
}
std::vector<std::string> aliases() const override { return {"castsequence"}; }
std::string helpText() const override { return "Cycle through a spell sequence"; }
};
// --- /use ---
class UseCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (ctx.args.empty()) return {false, false};
std::string useArg = ctx.args;
trimInPlace(useArg);
// Handle macro conditionals
if (!useArg.empty() && useArg.front() == '[') {
uint64_t dummy = static_cast<uint64_t>(-1);
useArg = evaluateMacroConditionals(useArg, ctx.gameHandler, dummy);
if (useArg.empty()) return {};
trimInPlace(useArg);
}
// Check for bag/slot notation: two numbers separated by whitespace
{
std::istringstream iss(useArg);
int bagNum = -1, slotNum = -1;
iss >> bagNum >> slotNum;
if (!iss.fail() && slotNum >= 1) {
if (bagNum == 0) {
ctx.gameHandler.useItemBySlot(slotNum - 1);
return {};
} else if (bagNum >= 1 && bagNum <= game::Inventory::NUM_BAG_SLOTS) {
ctx.gameHandler.useItemInBag(bagNum - 1, slotNum - 1);
return {};
}
}
}
// Numeric equip slot
{
uint32_t numId = parseNumericSpellId(useArg); // Reuse: strips '#', checks all digits
if (numId && numId >= 1 && numId <= static_cast<uint32_t>(game::EquipSlot::BAG4) + 1) {
auto eslot = static_cast<game::EquipSlot>(numId - 1);
const auto& esl = ctx.gameHandler.getInventory().getEquipSlot(eslot);
if (!esl.empty())
ctx.gameHandler.useItemById(esl.item.itemId);
return {};
}
}
// Name-based search
std::string useArgLower = useArg;
for (char& c : useArgLower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
bool found = false;
const auto& inv = ctx.gameHandler.getInventory();
// Search backpack
for (int s = 0; s < inv.getBackpackSize() && !found; ++s) {
const auto& slot = inv.getBackpackSlot(s);
if (slot.empty()) continue;
const auto* info = ctx.gameHandler.getItemInfo(slot.item.itemId);
if (!info) continue;
std::string nameLow = info->name;
for (char& c : nameLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (nameLow == useArgLower) {
ctx.gameHandler.useItemBySlot(s);
found = true;
}
}
// Search bags
for (int b = 0; b < game::Inventory::NUM_BAG_SLOTS && !found; ++b) {
for (int s = 0; s < inv.getBagSize(b) && !found; ++s) {
const auto& slot = inv.getBagSlot(b, s);
if (slot.empty()) continue;
const auto* info = ctx.gameHandler.getItemInfo(slot.item.itemId);
if (!info) continue;
std::string nameLow = info->name;
for (char& c : nameLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (nameLow == useArgLower) {
ctx.gameHandler.useItemInBag(b, s);
found = true;
}
}
}
if (!found) {
game::MessageChatData sysMsg;
sysMsg.type = game::ChatType::SYSTEM;
sysMsg.language = game::ChatLanguage::UNIVERSAL;
sysMsg.message = "Item not found: '" + useArg + "'.";
ctx.gameHandler.addLocalChatMessage(sysMsg);
}
return {};
}
std::vector<std::string> aliases() const override { return {"use"}; }
std::string helpText() const override { return "Use an item by name, ID, or bag/slot"; }
};
// --- /equip ---
class EquipCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (ctx.args.empty()) return {false, false};
std::string equipArg = ctx.args;
trimInPlace(equipArg);
std::string equipArgLower = equipArg;
for (char& c : equipArgLower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
bool found = false;
const auto& inv = ctx.gameHandler.getInventory();
for (int s = 0; s < inv.getBackpackSize() && !found; ++s) {
const auto& slot = inv.getBackpackSlot(s);
if (slot.empty()) continue;
const auto* info = ctx.gameHandler.getItemInfo(slot.item.itemId);
if (!info) continue;
std::string nameLow = info->name;
for (char& c : nameLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (nameLow == equipArgLower) {
ctx.gameHandler.autoEquipItemBySlot(s);
found = true;
}
}
for (int b = 0; b < game::Inventory::NUM_BAG_SLOTS && !found; ++b) {
for (int s = 0; s < inv.getBagSize(b) && !found; ++s) {
const auto& slot = inv.getBagSlot(b, s);
if (slot.empty()) continue;
const auto* info = ctx.gameHandler.getItemInfo(slot.item.itemId);
if (!info) continue;
std::string nameLow = info->name;
for (char& c : nameLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (nameLow == equipArgLower) {
ctx.gameHandler.autoEquipItemInBag(b, s);
found = true;
}
}
}
if (!found) {
game::MessageChatData sysMsg;
sysMsg.type = game::ChatType::SYSTEM;
sysMsg.language = game::ChatLanguage::UNIVERSAL;
sysMsg.message = "Item not found: '" + equipArg + "'.";
ctx.gameHandler.addLocalChatMessage(sysMsg);
}
return {};
}
std::vector<std::string> aliases() const override { return {"equip"}; }
std::string helpText() const override { return "Auto-equip an item from inventory"; }
};
// --- /equipset ---
class EquipSetCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
const auto& sets = ctx.gameHandler.getEquipmentSets();
auto sysSay = [&](const std::string& msg) {
game::MessageChatData m;
m.type = game::ChatType::SYSTEM;
m.language = game::ChatLanguage::UNIVERSAL;
m.message = msg;
ctx.gameHandler.addLocalChatMessage(m);
};
if (ctx.args.empty()) {
if (sets.empty()) {
sysSay("[System] No equipment sets saved.");
} else {
sysSay("[System] Equipment sets:");
for (const auto& es : sets)
sysSay(" " + es.name);
}
return {};
}
std::string setName = ctx.args;
trimInPlace(setName);
std::string setLower = setName;
std::transform(setLower.begin(), setLower.end(), setLower.begin(), ::tolower);
const game::GameHandler::EquipmentSetInfo* found = nullptr;
for (const auto& es : sets) {
std::string nameLow = es.name;
std::transform(nameLow.begin(), nameLow.end(), nameLow.begin(), ::tolower);
if (nameLow == setLower || nameLow.find(setLower) == 0) {
found = &es;
break;
}
}
if (found) {
ctx.gameHandler.useEquipmentSet(found->setId);
} else {
sysSay("[System] No equipment set matching '" + setName + "'.");
}
return {};
}
std::vector<std::string> aliases() const override { return {"equipset"}; }
std::string helpText() const override { return "Equip a saved equipment set"; }
};
// --- /startattack ---
class StartAttackCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
bool condPass = true;
uint64_t saOverride = static_cast<uint64_t>(-1);
if (!ctx.args.empty()) {
std::string saArg = ctx.args;
trimInPlace(saArg);
if (!saArg.empty() && saArg.front() == '[') {
std::string result = evaluateMacroConditionals(saArg, ctx.gameHandler, saOverride);
condPass = !(result.empty() && saOverride == static_cast<uint64_t>(-1));
}
}
if (condPass) {
uint64_t atkTarget = (saOverride != static_cast<uint64_t>(-1) && saOverride != 0)
? saOverride : (ctx.gameHandler.hasTarget() ? ctx.gameHandler.getTargetGuid() : 0);
if (atkTarget != 0) {
ctx.gameHandler.startAutoAttack(atkTarget);
} else {
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "You have no target.";
ctx.gameHandler.addLocalChatMessage(msg);
}
}
return {};
}
std::vector<std::string> aliases() const override { return {"startattack"}; }
std::string helpText() const override { return "Start auto-attack"; }
};
// --- /stopattack ---
class StopAttackCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.stopAutoAttack();
return {};
}
std::vector<std::string> aliases() const override { return {"stopattack"}; }
std::string helpText() const override { return "Stop auto-attack"; }
};
// --- /stopcasting ---
class StopCastingCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.stopCasting();
return {};
}
std::vector<std::string> aliases() const override { return {"stopcasting"}; }
std::string helpText() const override { return "Stop current cast"; }
};
// --- /cancelqueuedspell, /stopspellqueue ---
class CancelQueuedSpellCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.cancelQueuedSpell();
return {};
}
std::vector<std::string> aliases() const override { return {"cancelqueuedspell", "stopspellqueue"}; }
std::string helpText() const override { return "Cancel queued spell"; }
};
// --- Registration ---
void registerCombatCommands(ChatCommandRegistry& reg) {
reg.registerCommand(std::make_unique<CastCommand>());
reg.registerCommand(std::make_unique<CastSequenceCommand>());
reg.registerCommand(std::make_unique<UseCommand>());
reg.registerCommand(std::make_unique<EquipCommand>());
reg.registerCommand(std::make_unique<EquipSetCommand>());
reg.registerCommand(std::make_unique<StartAttackCommand>());
reg.registerCommand(std::make_unique<StopAttackCommand>());
reg.registerCommand(std::make_unique<StopCastingCommand>());
reg.registerCommand(std::make_unique<CancelQueuedSpellCommand>());
}
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,230 @@
// Emote/stance commands: /sit, /stand, /kneel, /dismount, /cancelform,
// /cancelaura, /cancellogout, /logout, /camp, /quit, /exit,
// pet commands (/petattack, /petfollow, etc.)
// Moved from ChatPanel::sendChatMessage() if/else chain (Phase 3).
#include "ui/chat/i_chat_command.hpp"
#include "ui/chat_panel.hpp"
#include "game/game_handler.hpp"
#include "rendering/renderer.hpp"
#include "rendering/animation_controller.hpp"
#include <algorithm>
#include <cctype>
namespace wowee { namespace ui {
// --- /sit ---
class SitCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.setStandState(1);
return {};
}
std::vector<std::string> aliases() const override { return {"sit"}; }
std::string helpText() const override { return "Sit down"; }
};
// --- /stand ---
class StandCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.setStandState(0);
return {};
}
std::vector<std::string> aliases() const override { return {"stand"}; }
std::string helpText() const override { return "Stand up"; }
};
// --- /kneel ---
class KneelCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.setStandState(8);
return {};
}
std::vector<std::string> aliases() const override { return {"kneel"}; }
std::string helpText() const override { return "Kneel"; }
};
// --- /logout, /camp, /quit, /exit ---
class LogoutEmoteCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.requestLogout();
return {};
}
std::vector<std::string> aliases() const override { return {"camp", "quit", "exit"}; }
std::string helpText() const override { return "Logout / quit game"; }
};
// --- /cancellogout ---
class CancelLogoutCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.cancelLogout();
return {};
}
std::vector<std::string> aliases() const override { return {"cancellogout"}; }
std::string helpText() const override { return "Cancel pending logout"; }
};
// --- /dismount ---
class DismountCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.dismount();
return {};
}
std::vector<std::string> aliases() const override { return {"dismount"}; }
std::string helpText() const override { return "Dismount"; }
};
// --- /cancelform, /cancelshapeshift ---
class CancelFormCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
for (const auto& aura : ctx.gameHandler.getPlayerAuras()) {
if (aura.spellId == 0) continue;
if (aura.flags & 0x20) {
ctx.gameHandler.cancelAura(aura.spellId);
break;
}
}
return {};
}
std::vector<std::string> aliases() const override { return {"cancelform", "cancelshapeshift"}; }
std::string helpText() const override { return "Cancel shapeshift form"; }
};
// --- /cancelaura ---
class CancelAuraCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (ctx.args.empty()) return {false, false};
std::string auraArg = ctx.args;
while (!auraArg.empty() && auraArg.front() == ' ') auraArg.erase(auraArg.begin());
while (!auraArg.empty() && auraArg.back() == ' ') auraArg.pop_back();
// Try numeric ID first
{
std::string numStr = auraArg;
if (!numStr.empty() && numStr.front() == '#') numStr.erase(numStr.begin());
bool isNum = !numStr.empty() &&
std::all_of(numStr.begin(), numStr.end(),
[](unsigned char c){ return std::isdigit(c); });
if (isNum) {
uint32_t spellId = 0;
try { spellId = static_cast<uint32_t>(std::stoul(numStr)); } catch (...) {}
if (spellId) ctx.gameHandler.cancelAura(spellId);
return {};
}
}
// Name match against player auras
std::string argLow = auraArg;
for (char& c : argLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
for (const auto& aura : ctx.gameHandler.getPlayerAuras()) {
if (aura.spellId == 0) continue;
std::string sn = ctx.gameHandler.getSpellName(aura.spellId);
for (char& c : sn) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (sn == argLow) {
ctx.gameHandler.cancelAura(aura.spellId);
break;
}
}
return {};
}
std::vector<std::string> aliases() const override { return {"cancelaura"}; }
std::string helpText() const override { return "Cancel a specific aura/buff"; }
};
// --- Pet commands ---
class PetAttackCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
uint64_t target = ctx.gameHandler.hasTarget() ? ctx.gameHandler.getTargetGuid() : 0;
ctx.gameHandler.sendPetAction(5, target);
return {};
}
std::vector<std::string> aliases() const override { return {"petattack"}; }
std::string helpText() const override { return "Pet: attack target"; }
};
class PetFollowCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.sendPetAction(2, 0);
return {};
}
std::vector<std::string> aliases() const override { return {"petfollow"}; }
std::string helpText() const override { return "Pet: follow owner"; }
};
class PetStayCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.sendPetAction(3, 0);
return {};
}
std::vector<std::string> aliases() const override { return {"petstay", "pethalt"}; }
std::string helpText() const override { return "Pet: stay"; }
};
class PetPassiveCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.sendPetAction(1, 0);
return {};
}
std::vector<std::string> aliases() const override { return {"petpassive"}; }
std::string helpText() const override { return "Pet: passive mode"; }
};
class PetDefensiveCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.sendPetAction(4, 0);
return {};
}
std::vector<std::string> aliases() const override { return {"petdefensive"}; }
std::string helpText() const override { return "Pet: defensive mode"; }
};
class PetAggressiveCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.sendPetAction(6, 0);
return {};
}
std::vector<std::string> aliases() const override { return {"petaggressive"}; }
std::string helpText() const override { return "Pet: aggressive mode"; }
};
class PetDismissCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.dismissPet();
return {};
}
std::vector<std::string> aliases() const override { return {"petdismiss"}; }
std::string helpText() const override { return "Dismiss pet"; }
};
// --- Registration ---
void registerEmoteCommands(ChatCommandRegistry& reg) {
reg.registerCommand(std::make_unique<SitCommand>());
reg.registerCommand(std::make_unique<StandCommand>());
reg.registerCommand(std::make_unique<KneelCommand>());
reg.registerCommand(std::make_unique<LogoutEmoteCommand>());
reg.registerCommand(std::make_unique<CancelLogoutCommand>());
reg.registerCommand(std::make_unique<DismountCommand>());
reg.registerCommand(std::make_unique<CancelFormCommand>());
reg.registerCommand(std::make_unique<CancelAuraCommand>());
reg.registerCommand(std::make_unique<PetAttackCommand>());
reg.registerCommand(std::make_unique<PetFollowCommand>());
reg.registerCommand(std::make_unique<PetStayCommand>());
reg.registerCommand(std::make_unique<PetPassiveCommand>());
reg.registerCommand(std::make_unique<PetDefensiveCommand>());
reg.registerCommand(std::make_unique<PetAggressiveCommand>());
reg.registerCommand(std::make_unique<PetDismissCommand>());
}
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,127 @@
// GM commands: /gmhelp, /gmcommands — local help for server-side dot-prefix commands.
// Also provides the gm_commands::getCompletions() function used by tab-completion.
// The actual GM commands (.gm, .tele, etc.) are sent to the server as SAY messages;
// the server (AzerothCore) does the real work. This file just adds discoverability.
#include "ui/chat/i_chat_command.hpp"
#include "ui/chat/chat_command_registry.hpp"
#include "ui/chat/gm_command_data.hpp"
#include "ui/chat/chat_utils.hpp"
#include "game/game_handler.hpp"
#include <algorithm>
#include <cctype>
#include <string>
#include <vector>
namespace wowee { namespace ui {
// ---------------------------------------------------------------------------
// gm_commands namespace — GM command lookup helpers used by tab-completion
// and the /gmhelp command.
// ---------------------------------------------------------------------------
namespace gm_commands {
std::vector<std::string> getCompletions(const std::string& prefix) {
std::vector<std::string> results;
for (const auto& cmd : kGmCommands) {
std::string dotName = "." + std::string(cmd.name);
if (dotName.size() >= prefix.size() &&
dotName.compare(0, prefix.size(), prefix) == 0) {
results.push_back(dotName);
}
}
std::sort(results.begin(), results.end());
return results;
}
const GmCommandEntry* find(const std::string& name) {
for (const auto& cmd : kGmCommands) {
if (cmd.name == name) return &cmd;
}
return nullptr;
}
} // namespace gm_commands
// ---------------------------------------------------------------------------
// /gmhelp [filter] — display GM command reference locally.
// ---------------------------------------------------------------------------
class GmHelpCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
std::string filter = ctx.args;
for (char& c : filter) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
// Trim leading/trailing whitespace
while (!filter.empty() && std::isspace(static_cast<unsigned char>(filter.front()))) filter.erase(filter.begin());
while (!filter.empty() && std::isspace(static_cast<unsigned char>(filter.back()))) filter.pop_back();
// If filter matches a specific command name, show detailed help
if (!filter.empty()) {
// Strip leading dot if user typed /gmhelp .gm
if (filter.front() == '.') filter = filter.substr(1);
bool found = false;
for (const auto& cmd : kGmCommands) {
std::string name(cmd.name);
if (name == filter || name.compare(0, filter.size(), filter) == 0) {
std::string line = std::string(cmd.syntax) + "" + std::string(cmd.help)
+ " [sec:" + std::to_string(cmd.security) + "]";
ctx.gameHandler.addLocalChatMessage(chat_utils::makeSystemMessage(line));
found = true;
}
}
if (!found) {
ctx.gameHandler.addLocalChatMessage(
chat_utils::makeSystemMessage("No GM commands matching '" + filter + "'."));
}
return {};
}
// No filter — print category overview
ctx.gameHandler.addLocalChatMessage(
chat_utils::makeSystemMessage("--- GM Commands (dot-prefix, sent to server) ---"));
ctx.gameHandler.addLocalChatMessage(
chat_utils::makeSystemMessage("GM Mode: .gm on/off .gm fly .gm visible"));
ctx.gameHandler.addLocalChatMessage(
chat_utils::makeSystemMessage("Teleport: .tele <loc> .go xyz .appear .summon"));
ctx.gameHandler.addLocalChatMessage(
chat_utils::makeSystemMessage("Character: .levelup .additem .learn .maxskill .pinfo"));
ctx.gameHandler.addLocalChatMessage(
chat_utils::makeSystemMessage("Combat: .revive .die .damage .freeze .respawn"));
ctx.gameHandler.addLocalChatMessage(
chat_utils::makeSystemMessage("Modify: .modify money/hp/mana/speed .morph .modify scale"));
ctx.gameHandler.addLocalChatMessage(
chat_utils::makeSystemMessage("Cheats: .cheat god/casttime/cooldown/power/taxi/explore"));
ctx.gameHandler.addLocalChatMessage(
chat_utils::makeSystemMessage("Spells: .cast .aura .unaura .cooldown .setskill"));
ctx.gameHandler.addLocalChatMessage(
chat_utils::makeSystemMessage("Quests: .quest add/complete/remove/reward/status"));
ctx.gameHandler.addLocalChatMessage(
chat_utils::makeSystemMessage("NPC: .npc add/delete/info/near/say/move"));
ctx.gameHandler.addLocalChatMessage(
chat_utils::makeSystemMessage("Objects: .gobject add/delete/info/near/target"));
ctx.gameHandler.addLocalChatMessage(
chat_utils::makeSystemMessage("Lookup: .lookup item/spell/creature/quest/area/teleport"));
ctx.gameHandler.addLocalChatMessage(
chat_utils::makeSystemMessage("Admin: .ban .kick .mute .announce .reload"));
ctx.gameHandler.addLocalChatMessage(
chat_utils::makeSystemMessage("Server: .server info .server motd .save .commands .help"));
ctx.gameHandler.addLocalChatMessage(
chat_utils::makeSystemMessage("Use /gmhelp <command> for details (e.g. /gmhelp tele)."));
ctx.gameHandler.addLocalChatMessage(
chat_utils::makeSystemMessage("Tab-complete works with dot-prefix (type .te<Tab>)."));
return {};
}
std::vector<std::string> aliases() const override { return {"gmhelp", "gmcommands"}; }
std::string helpText() const override { return "List GM dot-commands (server-side)"; }
};
// ---------------------------------------------------------------------------
// Registration
// ---------------------------------------------------------------------------
void registerGmCommands(ChatCommandRegistry& reg) {
reg.registerCommand(std::make_unique<GmHelpCommand>());
}
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,395 @@
// Group commands: /readycheck, /ready, /notready, /yield, /afk, /dnd,
// /uninvite, /leave, /maintank, /mainassist, /clearmaintank,
// /clearmainassist, /raidinfo, /raidconvert, /lootmethod,
// /lootthreshold, /mark, /roll
// Moved from ChatPanel::sendChatMessage() if/else chain (Phase 3).
#include "ui/chat/i_chat_command.hpp"
#include "ui/chat_panel.hpp"
#include "game/game_handler.hpp"
#include <algorithm>
#include <cctype>
namespace wowee { namespace ui {
// --- /readycheck, /rc ---
class ReadyCheckCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.initiateReadyCheck();
return {};
}
std::vector<std::string> aliases() const override { return {"readycheck", "rc"}; }
std::string helpText() const override { return "Initiate ready check"; }
};
// --- /ready ---
class ReadyCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.respondToReadyCheck(true);
return {};
}
std::vector<std::string> aliases() const override { return {"ready"}; }
std::string helpText() const override { return "Respond yes to ready check"; }
};
// --- /notready, /nr ---
class NotReadyCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.respondToReadyCheck(false);
return {};
}
std::vector<std::string> aliases() const override { return {"notready", "nr"}; }
std::string helpText() const override { return "Respond no to ready check"; }
};
// --- /yield, /forfeit, /surrender ---
class YieldCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.forfeitDuel();
return {};
}
std::vector<std::string> aliases() const override { return {"yield", "forfeit", "surrender"}; }
std::string helpText() const override { return "Forfeit current duel"; }
};
// --- /afk, /away ---
class AfkCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.toggleAfk(ctx.args);
return {};
}
std::vector<std::string> aliases() const override { return {"afk", "away"}; }
std::string helpText() const override { return "Toggle AFK status"; }
};
// --- /dnd, /busy ---
class DndCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.toggleDnd(ctx.args);
return {};
}
std::vector<std::string> aliases() const override { return {"dnd", "busy"}; }
std::string helpText() const override { return "Toggle Do Not Disturb"; }
};
// --- /uninvite, /kick ---
class UninviteCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (!ctx.args.empty()) {
ctx.gameHandler.uninvitePlayer(ctx.args);
} else {
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "Usage: /uninvite <player name>";
ctx.gameHandler.addLocalChatMessage(msg);
}
return {};
}
std::vector<std::string> aliases() const override { return {"uninvite", "kick"}; }
std::string helpText() const override { return "Remove player from group"; }
};
// --- /leave, /leaveparty ---
// /leave — leave party (no args) or leave channel (with args, WoW-style overload)
class LeavePartyCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (!ctx.args.empty()) {
// /leave ChannelName — leave a chat channel
ctx.gameHandler.leaveChannel(ctx.args);
} else {
ctx.gameHandler.leaveParty();
}
return {};
}
std::vector<std::string> aliases() const override { return {"leave", "leaveparty"}; }
std::string helpText() const override { return "Leave party/raid or channel"; }
};
// --- /maintank, /mt ---
class MainTankCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (ctx.gameHandler.hasTarget()) {
ctx.gameHandler.setMainTank(ctx.gameHandler.getTargetGuid());
} else {
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "You must target a player to set as main tank.";
ctx.gameHandler.addLocalChatMessage(msg);
}
return {};
}
std::vector<std::string> aliases() const override { return {"maintank", "mt"}; }
std::string helpText() const override { return "Set target as main tank"; }
};
// --- /mainassist, /ma ---
class MainAssistCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (ctx.gameHandler.hasTarget()) {
ctx.gameHandler.setMainAssist(ctx.gameHandler.getTargetGuid());
} else {
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "You must target a player to set as main assist.";
ctx.gameHandler.addLocalChatMessage(msg);
}
return {};
}
std::vector<std::string> aliases() const override { return {"mainassist", "ma"}; }
std::string helpText() const override { return "Set target as main assist"; }
};
// --- /clearmaintank ---
class ClearMainTankCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.clearMainTank();
return {};
}
std::vector<std::string> aliases() const override { return {"clearmaintank"}; }
std::string helpText() const override { return "Clear main tank assignment"; }
};
// --- /clearmainassist ---
class ClearMainAssistCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.clearMainAssist();
return {};
}
std::vector<std::string> aliases() const override { return {"clearmainassist"}; }
std::string helpText() const override { return "Clear main assist assignment"; }
};
// --- /raidinfo ---
class RaidInfoCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.requestRaidInfo();
return {};
}
std::vector<std::string> aliases() const override { return {"raidinfo"}; }
std::string helpText() const override { return "Show raid instance lockouts"; }
};
// --- /raidconvert ---
class RaidConvertCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.convertToRaid();
return {};
}
std::vector<std::string> aliases() const override { return {"raidconvert"}; }
std::string helpText() const override { return "Convert party to raid"; }
};
// --- /lootmethod, /grouploot, /setloot ---
class LootMethodCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (!ctx.gameHandler.isInGroup()) {
ctx.gameHandler.addUIError("You are not in a group.");
return {};
}
if (ctx.args.empty()) {
static constexpr const char* kMethodNames[] = {
"Free for All", "Round Robin", "Master Looter", "Group Loot", "Need Before Greed"
};
const auto& pd = ctx.gameHandler.getPartyData();
const char* cur = (pd.lootMethod < 5) ? kMethodNames[pd.lootMethod] : "Unknown";
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = std::string("Current loot method: ") + cur;
ctx.gameHandler.addLocalChatMessage(msg);
msg.message = "Usage: /lootmethod ffa|roundrobin|master|group|needbeforegreed";
ctx.gameHandler.addLocalChatMessage(msg);
return {};
}
std::string arg = ctx.args;
for (auto& c : arg) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
uint32_t method = 0xFFFFFFFF;
if (arg == "ffa" || arg == "freeforall") method = 0;
else if (arg == "roundrobin" || arg == "rr") method = 1;
else if (arg == "master" || arg == "masterloot") method = 2;
else if (arg == "group" || arg == "grouploot") method = 3;
else if (arg == "needbeforegreed" || arg == "nbg" || arg == "need") method = 4;
if (method == 0xFFFFFFFF) {
ctx.gameHandler.addUIError("Unknown loot method. Use: ffa, roundrobin, master, group, needbeforegreed");
} else {
const auto& pd = ctx.gameHandler.getPartyData();
uint64_t masterGuid = (method == 2) ? ctx.gameHandler.getPlayerGuid() : 0;
ctx.gameHandler.sendSetLootMethod(method, pd.lootThreshold, masterGuid);
}
return {};
}
std::vector<std::string> aliases() const override { return {"lootmethod", "grouploot", "setloot"}; }
std::string helpText() const override { return "Set loot method"; }
};
// --- /lootthreshold ---
class LootThresholdCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (!ctx.gameHandler.isInGroup()) {
ctx.gameHandler.addUIError("You are not in a group.");
return {};
}
if (ctx.args.empty()) {
const auto& pd = ctx.gameHandler.getPartyData();
static constexpr const char* kQualityNames[] = {
"Poor (grey)", "Common (white)", "Uncommon (green)",
"Rare (blue)", "Epic (purple)", "Legendary (orange)"
};
const char* cur = (pd.lootThreshold < 6) ? kQualityNames[pd.lootThreshold] : "Unknown";
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = std::string("Current loot threshold: ") + cur;
ctx.gameHandler.addLocalChatMessage(msg);
msg.message = "Usage: /lootthreshold <0-5> (0=Poor, 1=Common, 2=Uncommon, 3=Rare, 4=Epic, 5=Legendary)";
ctx.gameHandler.addLocalChatMessage(msg);
return {};
}
std::string arg = ctx.args;
while (!arg.empty() && arg.front() == ' ') arg.erase(arg.begin());
uint32_t threshold = 0xFFFFFFFF;
if (arg.size() == 1 && arg[0] >= '0' && arg[0] <= '5') {
threshold = static_cast<uint32_t>(arg[0] - '0');
} else {
for (auto& c : arg) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (arg == "poor" || arg == "grey" || arg == "gray") threshold = 0;
else if (arg == "common" || arg == "white") threshold = 1;
else if (arg == "uncommon" || arg == "green") threshold = 2;
else if (arg == "rare" || arg == "blue") threshold = 3;
else if (arg == "epic" || arg == "purple") threshold = 4;
else if (arg == "legendary" || arg == "orange") threshold = 5;
}
if (threshold == 0xFFFFFFFF) {
ctx.gameHandler.addUIError("Invalid threshold. Use 0-5 or: poor, common, uncommon, rare, epic, legendary");
} else {
const auto& pd = ctx.gameHandler.getPartyData();
uint64_t masterGuid = (pd.lootMethod == 2) ? ctx.gameHandler.getPlayerGuid() : 0;
ctx.gameHandler.sendSetLootMethod(pd.lootMethod, threshold, masterGuid);
}
return {};
}
std::vector<std::string> aliases() const override { return {"lootthreshold"}; }
std::string helpText() const override { return "Set loot quality threshold"; }
};
// --- /mark, /marktarget, /raidtarget ---
class MarkCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (!ctx.gameHandler.hasTarget()) {
game::MessageChatData noTgt;
noTgt.type = game::ChatType::SYSTEM;
noTgt.language = game::ChatLanguage::UNIVERSAL;
noTgt.message = "No target selected.";
ctx.gameHandler.addLocalChatMessage(noTgt);
return {};
}
static constexpr const char* kMarkWords[] = {
"star", "circle", "diamond", "triangle", "moon", "square", "cross", "skull"
};
uint8_t icon = 7; // default: skull
if (!ctx.args.empty()) {
std::string argLow = ctx.args;
for (auto& c : argLow) c = static_cast<char>(std::tolower(c));
while (!argLow.empty() && argLow.front() == ' ') argLow.erase(argLow.begin());
if (argLow == "clear" || argLow == "0" || argLow == "none") {
ctx.gameHandler.setRaidMark(ctx.gameHandler.getTargetGuid(), 0xFF);
return {};
}
bool found = false;
for (int mi = 0; mi < 8; ++mi) {
if (argLow == kMarkWords[mi]) { icon = static_cast<uint8_t>(mi); found = true; break; }
}
if (!found && !argLow.empty() && argLow[0] >= '1' && argLow[0] <= '8') {
icon = static_cast<uint8_t>(argLow[0] - '1');
found = true;
}
if (!found) {
game::MessageChatData badArg;
badArg.type = game::ChatType::SYSTEM;
badArg.language = game::ChatLanguage::UNIVERSAL;
badArg.message = "Unknown mark. Use: star circle diamond triangle moon square cross skull";
ctx.gameHandler.addLocalChatMessage(badArg);
return {};
}
}
ctx.gameHandler.setRaidMark(ctx.gameHandler.getTargetGuid(), icon);
return {};
}
std::vector<std::string> aliases() const override { return {"mark", "marktarget", "raidtarget"}; }
std::string helpText() const override { return "Set raid target mark on target"; }
};
// --- /roll, /random, /rnd ---
class RollCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
uint32_t minRoll = 1;
uint32_t maxRoll = 100;
if (!ctx.args.empty()) {
size_t dashPos = ctx.args.find('-');
size_t spacePos = ctx.args.find(' ');
if (dashPos != std::string::npos) {
try {
minRoll = std::stoul(ctx.args.substr(0, dashPos));
maxRoll = std::stoul(ctx.args.substr(dashPos + 1));
} catch (...) {}
} else if (spacePos != std::string::npos) {
try {
minRoll = std::stoul(ctx.args.substr(0, spacePos));
maxRoll = std::stoul(ctx.args.substr(spacePos + 1));
} catch (...) {}
} else {
try { maxRoll = std::stoul(ctx.args); } catch (...) {}
}
}
ctx.gameHandler.randomRoll(minRoll, maxRoll);
return {};
}
std::vector<std::string> aliases() const override { return {"roll", "random", "rnd"}; }
std::string helpText() const override { return "Random dice roll"; }
};
// --- Registration ---
void registerGroupCommands(ChatCommandRegistry& reg) {
reg.registerCommand(std::make_unique<ReadyCheckCommand>());
reg.registerCommand(std::make_unique<ReadyCommand>());
reg.registerCommand(std::make_unique<NotReadyCommand>());
reg.registerCommand(std::make_unique<YieldCommand>());
reg.registerCommand(std::make_unique<AfkCommand>());
reg.registerCommand(std::make_unique<DndCommand>());
reg.registerCommand(std::make_unique<UninviteCommand>());
reg.registerCommand(std::make_unique<LeavePartyCommand>());
reg.registerCommand(std::make_unique<MainTankCommand>());
reg.registerCommand(std::make_unique<MainAssistCommand>());
reg.registerCommand(std::make_unique<ClearMainTankCommand>());
reg.registerCommand(std::make_unique<ClearMainAssistCommand>());
reg.registerCommand(std::make_unique<RaidInfoCommand>());
reg.registerCommand(std::make_unique<RaidConvertCommand>());
reg.registerCommand(std::make_unique<LootMethodCommand>());
reg.registerCommand(std::make_unique<LootThresholdCommand>());
reg.registerCommand(std::make_unique<MarkCommand>());
reg.registerCommand(std::make_unique<RollCommand>());
}
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,203 @@
// Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit,
// /ginvite, /gkick, /gcreate, /gdisband, /gleader
// Moved from ChatPanel::sendChatMessage() if/else chain (Phase 3).
#include "ui/chat/i_chat_command.hpp"
#include "ui/chat_panel.hpp"
#include "game/game_handler.hpp"
namespace wowee { namespace ui {
// --- /ginfo, /guildinfo ---
class GuildInfoCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.requestGuildInfo();
return {};
}
std::vector<std::string> aliases() const override { return {"ginfo", "guildinfo"}; }
std::string helpText() const override { return "Show guild info"; }
};
// --- /groster, /guildroster ---
class GuildRosterCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.requestGuildRoster();
return {};
}
std::vector<std::string> aliases() const override { return {"groster", "guildroster"}; }
std::string helpText() const override { return "Show guild roster"; }
};
// --- /gmotd, /guildmotd ---
class GuildMotdCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (!ctx.args.empty()) {
ctx.gameHandler.setGuildMotd(ctx.args);
return {};
}
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "Usage: /gmotd <message>";
ctx.gameHandler.addLocalChatMessage(msg);
return {};
}
std::vector<std::string> aliases() const override { return {"gmotd", "guildmotd"}; }
std::string helpText() const override { return "Set guild message of the day"; }
};
// --- /gpromote, /guildpromote ---
class GuildPromoteCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (!ctx.args.empty()) {
ctx.gameHandler.promoteGuildMember(ctx.args);
return {};
}
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "Usage: /gpromote <player>";
ctx.gameHandler.addLocalChatMessage(msg);
return {};
}
std::vector<std::string> aliases() const override { return {"gpromote", "guildpromote"}; }
std::string helpText() const override { return "Promote guild member"; }
};
// --- /gdemote, /guilddemote ---
class GuildDemoteCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (!ctx.args.empty()) {
ctx.gameHandler.demoteGuildMember(ctx.args);
return {};
}
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "Usage: /gdemote <player>";
ctx.gameHandler.addLocalChatMessage(msg);
return {};
}
std::vector<std::string> aliases() const override { return {"gdemote", "guilddemote"}; }
std::string helpText() const override { return "Demote guild member"; }
};
// --- /gquit, /guildquit, /leaveguild ---
class GuildQuitCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.leaveGuild();
return {};
}
std::vector<std::string> aliases() const override { return {"gquit", "guildquit", "leaveguild"}; }
std::string helpText() const override { return "Leave guild"; }
};
// --- /ginvite, /guildinvite ---
class GuildInviteCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (!ctx.args.empty()) {
ctx.gameHandler.inviteToGuild(ctx.args);
return {};
}
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "Usage: /ginvite <player>";
ctx.gameHandler.addLocalChatMessage(msg);
return {};
}
std::vector<std::string> aliases() const override { return {"ginvite", "guildinvite"}; }
std::string helpText() const override { return "Invite player to guild"; }
};
// --- /gkick, /guildkick ---
class GuildKickCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (!ctx.args.empty()) {
ctx.gameHandler.kickGuildMember(ctx.args);
return {};
}
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "Usage: /gkick <player>";
ctx.gameHandler.addLocalChatMessage(msg);
return {};
}
std::vector<std::string> aliases() const override { return {"gkick", "guildkick"}; }
std::string helpText() const override { return "Kick player from guild"; }
};
// --- /gcreate, /guildcreate ---
class GuildCreateCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (!ctx.args.empty()) {
ctx.gameHandler.createGuild(ctx.args);
return {};
}
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "Usage: /gcreate <guild name>";
ctx.gameHandler.addLocalChatMessage(msg);
return {};
}
std::vector<std::string> aliases() const override { return {"gcreate", "guildcreate"}; }
std::string helpText() const override { return "Create a guild"; }
};
// --- /gdisband, /guilddisband ---
class GuildDisbandCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.disbandGuild();
return {};
}
std::vector<std::string> aliases() const override { return {"gdisband", "guilddisband"}; }
std::string helpText() const override { return "Disband guild"; }
};
// --- /gleader, /guildleader ---
class GuildLeaderCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (!ctx.args.empty()) {
ctx.gameHandler.setGuildLeader(ctx.args);
return {};
}
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "Usage: /gleader <player>";
ctx.gameHandler.addLocalChatMessage(msg);
return {};
}
std::vector<std::string> aliases() const override { return {"gleader", "guildleader"}; }
std::string helpText() const override { return "Transfer guild leadership"; }
};
// --- Registration ---
void registerGuildCommands(ChatCommandRegistry& reg) {
reg.registerCommand(std::make_unique<GuildInfoCommand>());
reg.registerCommand(std::make_unique<GuildRosterCommand>());
reg.registerCommand(std::make_unique<GuildMotdCommand>());
reg.registerCommand(std::make_unique<GuildPromoteCommand>());
reg.registerCommand(std::make_unique<GuildDemoteCommand>());
reg.registerCommand(std::make_unique<GuildQuitCommand>());
reg.registerCommand(std::make_unique<GuildInviteCommand>());
reg.registerCommand(std::make_unique<GuildKickCommand>());
reg.registerCommand(std::make_unique<GuildCreateCommand>());
reg.registerCommand(std::make_unique<GuildDisbandCommand>());
reg.registerCommand(std::make_unique<GuildLeaderCommand>());
}
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,124 @@
// Help commands: /help, /chathelp, /macrohelp
// Moved from ChatPanel::sendChatMessage() if/else chain (Phase 3).
#include "ui/chat/i_chat_command.hpp"
#include "ui/chat_panel.hpp"
#include "game/game_handler.hpp"
namespace wowee { namespace ui {
// --- /help, /? ---
class HelpCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
static constexpr const char* kHelpLines[] = {
"--- Wowee Slash Commands ---",
"Chat: /s /y /p /g /raid /rw /o /bg /w <name> /r /join /leave",
"Social: /who /friend add/remove /ignore /unignore",
"Party: /invite /uninvite /leave /readycheck /mark /roll",
" /maintank /mainassist /raidconvert /raidinfo",
" /lootmethod /lootthreshold",
"Guild: /ginvite /gkick /gquit /gpromote /gdemote /gmotd",
" /gleader /groster /ginfo /gcreate /gdisband",
"Combat: /cast /castsequence /use /startattack /stopattack",
" /stopcasting /duel /forfeit /pvp /assist",
" /follow /stopfollow /threat /combatlog",
"Items: /use <item> /equip <item> /equipset [name]",
"Target: /target /cleartarget /focus /clearfocus /inspect",
"Movement: /sit /stand /kneel /dismount",
"Misc: /played /time /zone /loc /afk /dnd /helm /cloak",
" /trade /score /unstuck /logout /quit /exit /ticket",
" /screenshot /difficulty",
" /macrohelp /chathelp /help /gmhelp",
"GM: .command (dot-prefix sent to server, /gmhelp for list)",
};
for (const char* line : kHelpLines) {
game::MessageChatData helpMsg;
helpMsg.type = game::ChatType::SYSTEM;
helpMsg.language = game::ChatLanguage::UNIVERSAL;
helpMsg.message = line;
ctx.gameHandler.addLocalChatMessage(helpMsg);
}
return {};
}
std::vector<std::string> aliases() const override { return {"help", "?"}; }
std::string helpText() const override { return "List all slash commands"; }
};
// --- /chathelp ---
class ChatHelpCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
static constexpr const char* kChatHelp[] = {
"--- Chat Channel Commands ---",
"/s [msg] Say to nearby players",
"/y [msg] Yell to a wider area",
"/w <name> [msg] Whisper to player",
"/r [msg] Reply to last whisper",
"/p [msg] Party chat",
"/g [msg] Guild chat",
"/o [msg] Guild officer chat",
"/raid [msg] Raid chat",
"/rw [msg] Raid warning",
"/bg [msg] Battleground chat",
"/1 [msg] General channel",
"/2 [msg] Trade channel (also /wts /wtb)",
"/<N> [msg] Channel by number",
"/join <chan> Join a channel",
"/leave <chan> Leave a channel",
"/afk [msg] Set AFK status",
"/dnd [msg] Set Do Not Disturb",
};
for (const char* line : kChatHelp) {
game::MessageChatData helpMsg;
helpMsg.type = game::ChatType::SYSTEM;
helpMsg.language = game::ChatLanguage::UNIVERSAL;
helpMsg.message = line;
ctx.gameHandler.addLocalChatMessage(helpMsg);
}
return {};
}
std::vector<std::string> aliases() const override { return {"chathelp"}; }
std::string helpText() const override { return "List chat channel commands"; }
};
// --- /macrohelp ---
class MacroHelpCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
static constexpr const char* kMacroHelp[] = {
"--- Macro Conditionals ---",
"Usage: /cast [cond1,cond2] Spell1; [cond3] Spell2; Default",
"State: [combat] [mounted] [swimming] [flying] [stealthed]",
" [channeling] [pet] [group] [raid] [indoors] [outdoors]",
"Spec: [spec:1] [spec:2] (active talent spec, 1-based)",
" (prefix no- to negate any condition)",
"Target: [harm] [help] [exists] [noexists] [dead] [nodead]",
" [target=focus] [target=pet] [target=mouseover] [target=player]",
" (also: @focus, @pet, @mouseover, @player, @target)",
"Form: [noform] [nostance] [form:0]",
"Keys: [mod:shift] [mod:ctrl] [mod:alt]",
"Aura: [buff:Name] [nobuff:Name] [debuff:Name] [nodebuff:Name]",
"Other: #showtooltip, /stopmacro [cond], /castsequence",
};
for (const char* line : kMacroHelp) {
game::MessageChatData m;
m.type = game::ChatType::SYSTEM;
m.language = game::ChatLanguage::UNIVERSAL;
m.message = line;
ctx.gameHandler.addLocalChatMessage(m);
}
return {};
}
std::vector<std::string> aliases() const override { return {"macrohelp"}; }
std::string helpText() const override { return "List macro conditionals"; }
};
// --- Registration ---
void registerHelpCommands(ChatCommandRegistry& reg) {
reg.registerCommand(std::make_unique<HelpCommand>());
reg.registerCommand(std::make_unique<ChatHelpCommand>());
reg.registerCommand(std::make_unique<MacroHelpCommand>());
}
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,404 @@
// Misc commands: /time, /loc, /zone, /played, /screenshot, /ticket, /score,
// /threat, /combatlog, /helm, /cloak, /follow, /stopfollow,
// /assist, /pvp, /unstuck*, /transport
// Moved from ChatPanel::sendChatMessage() if/else chain (Phase 3).
#include "ui/chat/i_chat_command.hpp"
#include "ui/chat_panel.hpp"
#include "game/game_handler.hpp"
#include "game/entity.hpp"
#include "rendering/renderer.hpp"
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <limits>
#include <memory>
#include <glm/vec3.hpp>
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 {
inline std::string getEntityName(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";
}
} // anon namespace
// --- /time ---
class TimeCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.queryServerTime();
return {};
}
std::vector<std::string> aliases() const override { return {"time"}; }
std::string helpText() const override { return "Query server time"; }
};
// --- /loc, /coords, /whereami ---
class LocCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
const auto& pmi = ctx.gameHandler.getMovementInfo();
std::string zoneName;
if (auto* rend = ctx.services.renderer)
zoneName = rend->getCurrentZoneName();
char buf[256];
snprintf(buf, sizeof(buf), "%.1f, %.1f, %.1f%s%s",
pmi.x, pmi.y, pmi.z,
zoneName.empty() ? "" : "",
zoneName.c_str());
game::MessageChatData sysMsg;
sysMsg.type = game::ChatType::SYSTEM;
sysMsg.language = game::ChatLanguage::UNIVERSAL;
sysMsg.message = buf;
ctx.gameHandler.addLocalChatMessage(sysMsg);
return {};
}
std::vector<std::string> aliases() const override { return {"loc", "coords", "whereami"}; }
std::string helpText() const override { return "Print player coordinates"; }
};
// --- /zone ---
class ZoneCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
std::string zoneName;
if (auto* rend = ctx.services.renderer)
zoneName = rend->getCurrentZoneName();
game::MessageChatData sysMsg;
sysMsg.type = game::ChatType::SYSTEM;
sysMsg.language = game::ChatLanguage::UNIVERSAL;
sysMsg.message = zoneName.empty() ? "You are not in a known zone." : "You are in: " + zoneName;
ctx.gameHandler.addLocalChatMessage(sysMsg);
return {};
}
std::vector<std::string> aliases() const override { return {"zone"}; }
std::string helpText() const override { return "Show current zone"; }
};
// --- /played ---
class PlayedCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.requestPlayedTime();
return {};
}
std::vector<std::string> aliases() const override { return {"played"}; }
std::string helpText() const override { return "Show time played"; }
};
// --- /screenshot, /ss ---
class ScreenshotCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.panel.getSlashCmds().takeScreenshot = true;
return {};
}
std::vector<std::string> aliases() const override { return {"screenshot", "ss"}; }
std::string helpText() const override { return "Take a screenshot"; }
};
// --- /ticket, /gmticket, /gm ---
class TicketCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.panel.getSlashCmds().showGmTicket = true;
return {};
}
std::vector<std::string> aliases() const override { return {"ticket", "gmticket", "gm"}; }
std::string helpText() const override { return "Open GM ticket"; }
};
// --- /score ---
class ScoreCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.requestPvpLog();
ctx.panel.getSlashCmds().showBgScore = true;
return {};
}
std::vector<std::string> aliases() const override { return {"score"}; }
std::string helpText() const override { return "Show BG scoreboard"; }
};
// --- /threat ---
class ThreatCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.panel.getSlashCmds().toggleThreat = true;
return {};
}
std::vector<std::string> aliases() const override { return {"threat"}; }
std::string helpText() const override { return "Toggle threat display"; }
};
// --- /combatlog, /cl ---
class CombatLogCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.panel.getSlashCmds().toggleCombatLog = true;
return {};
}
std::vector<std::string> aliases() const override { return {"combatlog", "cl"}; }
std::string helpText() const override { return "Toggle combat log"; }
};
// --- /helm, /helmet, /showhelm ---
class HelmCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.toggleHelm();
return {};
}
std::vector<std::string> aliases() const override { return {"helm", "helmet", "showhelm"}; }
std::string helpText() const override { return "Toggle helmet visibility"; }
};
// --- /cloak, /showcloak ---
class CloakCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.toggleCloak();
return {};
}
std::vector<std::string> aliases() const override { return {"cloak", "showcloak"}; }
std::string helpText() const override { return "Toggle cloak visibility"; }
};
// --- /follow, /f ---
class FollowCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.followTarget();
return {};
}
std::vector<std::string> aliases() const override { return {"follow", "f"}; }
std::string helpText() const override { return "Follow target"; }
};
// --- /stopfollow ---
class StopFollowCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.cancelFollow();
return {};
}
std::vector<std::string> aliases() const override { return {"stopfollow"}; }
std::string helpText() const override { return "Stop following"; }
};
// --- /assist ---
class AssistCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
auto assistEntityTarget = [&](uint64_t srcGuid) {
auto srcEnt = ctx.gameHandler.getEntityManager().getEntity(srcGuid);
if (!srcEnt) { ctx.gameHandler.assistTarget(); return; }
uint64_t atkGuid = 0;
const auto& flds = srcEnt->getFields();
auto iLo = flds.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_LO));
if (iLo != flds.end()) {
atkGuid = iLo->second;
auto iHi = flds.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_HI));
if (iHi != flds.end()) atkGuid |= (static_cast<uint64_t>(iHi->second) << 32);
}
if (atkGuid != 0) {
ctx.gameHandler.setTarget(atkGuid);
} else {
std::string sn = getEntityName(srcEnt);
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = (sn.empty() ? "Target" : sn) + " has no target.";
ctx.gameHandler.addLocalChatMessage(msg);
}
};
if (!ctx.args.empty()) {
std::string assistArg = ctx.args;
while (!assistArg.empty() && assistArg.front() == ' ') assistArg.erase(assistArg.begin());
// Evaluate conditionals if present
uint64_t assistOver = static_cast<uint64_t>(-1);
if (!assistArg.empty() && assistArg.front() == '[') {
assistArg = evaluateMacroConditionals(assistArg, ctx.gameHandler, assistOver);
if (assistArg.empty() && assistOver == static_cast<uint64_t>(-1)) return {};
while (!assistArg.empty() && assistArg.front() == ' ') assistArg.erase(assistArg.begin());
while (!assistArg.empty() && assistArg.back() == ' ') assistArg.pop_back();
}
if (assistOver != static_cast<uint64_t>(-1) && assistOver != 0) {
assistEntityTarget(assistOver);
} else if (!assistArg.empty()) {
// Name search
std::string argLow = assistArg;
for (char& c : argLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
uint64_t bestGuid = 0; float bestDist = std::numeric_limits<float>::max();
const auto& pmi = ctx.gameHandler.getMovementInfo();
for (const auto& [guid, ent] : ctx.gameHandler.getEntityManager().getEntities()) {
if (!ent || ent->getType() == game::ObjectType::OBJECT) continue;
std::string nm = getEntityName(ent);
std::string nml = nm;
for (char& c : nml) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (nml.find(argLow) != 0) continue;
float d2 = (ent->getX()-pmi.x)*(ent->getX()-pmi.x)
+ (ent->getY()-pmi.y)*(ent->getY()-pmi.y);
if (d2 < bestDist) { bestDist = d2; bestGuid = guid; }
}
if (bestGuid) assistEntityTarget(bestGuid);
else {
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "No unit matching '" + assistArg + "' found.";
ctx.gameHandler.addLocalChatMessage(msg);
}
} else {
ctx.gameHandler.assistTarget();
}
} else {
ctx.gameHandler.assistTarget();
}
return {};
}
std::vector<std::string> aliases() const override { return {"assist"}; }
std::string helpText() const override { return "Assist target (target their target)"; }
};
// --- /pvp ---
class PvpCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.togglePvp();
return {};
}
std::vector<std::string> aliases() const override { return {"pvp"}; }
std::string helpText() const override { return "Toggle PvP flag"; }
};
// --- /unstuck ---
class UnstuckCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.unstuck();
return {};
}
std::vector<std::string> aliases() const override { return {"unstuck"}; }
std::string helpText() const override { return "Reset position to floor height"; }
};
// --- /unstuckgy ---
class UnstuckGyCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.unstuckGy();
return {};
}
std::vector<std::string> aliases() const override { return {"unstuckgy"}; }
std::string helpText() const override { return "Move to nearest graveyard"; }
};
// --- /unstuckhearth ---
class UnstuckHearthCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.unstuckHearth();
return {};
}
std::vector<std::string> aliases() const override { return {"unstuckhearth"}; }
std::string helpText() const override { return "Teleport to hearthstone bind point"; }
};
// --- /transport board ---
class TransportBoardCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
// This is invoked via the "transport" alias. Check args for sub-command.
std::string sub = ctx.args;
while (!sub.empty() && sub.front() == ' ') sub.erase(sub.begin());
for (char& c : sub) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (sub == "board") {
auto* tm = ctx.gameHandler.getTransportManager();
if (tm) {
uint64_t testTransportGuid = 0x1000000000000001ULL;
glm::vec3 deckCenter(0.0f, 0.0f, 5.0f);
ctx.gameHandler.setPlayerOnTransport(testTransportGuid, deckCenter);
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "Boarded test transport. Use '/transport leave' to disembark.";
ctx.gameHandler.addLocalChatMessage(msg);
} else {
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "Transport system not available.";
ctx.gameHandler.addLocalChatMessage(msg);
}
return {};
} else if (sub == "leave") {
if (ctx.gameHandler.isOnTransport()) {
ctx.gameHandler.clearPlayerTransport();
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "Disembarked from transport.";
ctx.gameHandler.addLocalChatMessage(msg);
} else {
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "You are not on a transport.";
ctx.gameHandler.addLocalChatMessage(msg);
}
return {};
}
// Unrecognized sub-command
return {false, false};
}
std::vector<std::string> aliases() const override { return {"transport"}; }
std::string helpText() const override { return "Transport: /transport board|leave"; }
};
// --- Registration ---
void registerMiscCommands(ChatCommandRegistry& reg) {
reg.registerCommand(std::make_unique<TimeCommand>());
reg.registerCommand(std::make_unique<LocCommand>());
reg.registerCommand(std::make_unique<ZoneCommand>());
reg.registerCommand(std::make_unique<PlayedCommand>());
reg.registerCommand(std::make_unique<ScreenshotCommand>());
reg.registerCommand(std::make_unique<TicketCommand>());
reg.registerCommand(std::make_unique<ScoreCommand>());
reg.registerCommand(std::make_unique<ThreatCommand>());
reg.registerCommand(std::make_unique<CombatLogCommand>());
reg.registerCommand(std::make_unique<HelmCommand>());
reg.registerCommand(std::make_unique<CloakCommand>());
reg.registerCommand(std::make_unique<FollowCommand>());
reg.registerCommand(std::make_unique<StopFollowCommand>());
reg.registerCommand(std::make_unique<AssistCommand>());
reg.registerCommand(std::make_unique<PvpCommand>());
reg.registerCommand(std::make_unique<UnstuckCommand>());
reg.registerCommand(std::make_unique<UnstuckGyCommand>());
reg.registerCommand(std::make_unique<UnstuckHearthCommand>());
reg.registerCommand(std::make_unique<TransportBoardCommand>());
}
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,211 @@
// Social commands: /friend, /removefriend, /ignore, /unignore, /invite, /inspect, /who
// Moved from ChatPanel::sendChatMessage() if/else chain (Phase 3).
#include "ui/chat/i_chat_command.hpp"
#include "ui/chat_panel.hpp"
#include "game/game_handler.hpp"
#include <algorithm>
#include <cctype>
namespace wowee { namespace ui {
// --- /invite ---
class InviteCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (ctx.args.empty()) return {false, false};
ctx.gameHandler.inviteToGroup(ctx.args);
return {};
}
std::vector<std::string> aliases() const override { return {"invite"}; }
std::string helpText() const override { return "Invite player to group"; }
};
// --- /inspect ---
class InspectCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.inspectTarget();
ctx.panel.getSlashCmds().showInspect = true;
return {};
}
std::vector<std::string> aliases() const override { return {"inspect"}; }
std::string helpText() const override { return "Inspect target's equipment"; }
};
// --- /friend, /addfriend ---
class FriendCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (!ctx.args.empty()) {
size_t subCmdSpace = ctx.args.find(' ');
if (ctx.fullCommand == "friend" && subCmdSpace != std::string::npos) {
std::string subCmd = ctx.args.substr(0, subCmdSpace);
std::transform(subCmd.begin(), subCmd.end(), subCmd.begin(), ::tolower);
if (subCmd == "add") {
ctx.gameHandler.addFriend(ctx.args.substr(subCmdSpace + 1));
return {};
} else if (subCmd == "remove" || subCmd == "delete" || subCmd == "rem") {
ctx.gameHandler.removeFriend(ctx.args.substr(subCmdSpace + 1));
return {};
}
} else {
// /addfriend name or /friend name (assume add)
ctx.gameHandler.addFriend(ctx.args);
return {};
}
}
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "Usage: /friend add <name> or /friend remove <name>";
ctx.gameHandler.addLocalChatMessage(msg);
return {};
}
std::vector<std::string> aliases() const override { return {"friend", "addfriend"}; }
std::string helpText() const override { return "Add/remove friend"; }
};
// --- /removefriend, /delfriend, /remfriend ---
class RemoveFriendCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (!ctx.args.empty()) {
ctx.gameHandler.removeFriend(ctx.args);
return {};
}
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "Usage: /removefriend <name>";
ctx.gameHandler.addLocalChatMessage(msg);
return {};
}
std::vector<std::string> aliases() const override { return {"removefriend", "delfriend", "remfriend"}; }
std::string helpText() const override { return "Remove friend"; }
};
// --- /ignore ---
class IgnoreCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (!ctx.args.empty()) {
ctx.gameHandler.addIgnore(ctx.args);
return {};
}
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "Usage: /ignore <name>";
ctx.gameHandler.addLocalChatMessage(msg);
return {};
}
std::vector<std::string> aliases() const override { return {"ignore"}; }
std::string helpText() const override { return "Ignore player messages"; }
};
// --- /unignore ---
class UnignoreCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (!ctx.args.empty()) {
ctx.gameHandler.removeIgnore(ctx.args);
return {};
}
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "Usage: /unignore <name>";
ctx.gameHandler.addLocalChatMessage(msg);
return {};
}
std::vector<std::string> aliases() const override { return {"unignore"}; }
std::string helpText() const override { return "Unignore player"; }
};
// --- /who, /whois, /online, /players ---
class WhoCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
std::string query = ctx.args;
// Trim
size_t first = query.find_first_not_of(" \t\r\n");
if (first == std::string::npos) query.clear();
else { size_t last = query.find_last_not_of(" \t\r\n"); query = query.substr(first, last - first + 1); }
if (ctx.fullCommand == "whois" && query.empty()) {
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "Usage: /whois <playerName>";
ctx.gameHandler.addLocalChatMessage(msg);
return {};
}
if (ctx.fullCommand == "who" && (query == "help" || query == "?")) {
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "Who commands: /who [name/filter], /whois <name>, /online";
ctx.gameHandler.addLocalChatMessage(msg);
return {};
}
ctx.gameHandler.queryWho(query);
ctx.panel.getSlashCmds().showWho = true;
return {};
}
std::vector<std::string> aliases() const override { return {"who", "whois", "online", "players"}; }
std::string helpText() const override { return "List online players"; }
};
// --- /duel ---
class DuelCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (ctx.gameHandler.hasTarget()) {
ctx.gameHandler.proposeDuel(ctx.gameHandler.getTargetGuid());
} else {
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "You must target a player to challenge to a duel.";
ctx.gameHandler.addLocalChatMessage(msg);
}
return {};
}
std::vector<std::string> aliases() const override { return {"duel"}; }
std::string helpText() const override { return "Challenge target to duel"; }
};
// --- /trade ---
class TradeCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (ctx.gameHandler.hasTarget()) {
ctx.gameHandler.initiateTrade(ctx.gameHandler.getTargetGuid());
} else {
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "You must target a player to trade with.";
ctx.gameHandler.addLocalChatMessage(msg);
}
return {};
}
std::vector<std::string> aliases() const override { return {"trade"}; }
std::string helpText() const override { return "Initiate trade with target"; }
};
// --- Registration ---
void registerSocialCommands(ChatCommandRegistry& reg) {
reg.registerCommand(std::make_unique<InviteCommand>());
reg.registerCommand(std::make_unique<InspectCommand>());
reg.registerCommand(std::make_unique<FriendCommand>());
reg.registerCommand(std::make_unique<RemoveFriendCommand>());
reg.registerCommand(std::make_unique<IgnoreCommand>());
reg.registerCommand(std::make_unique<UnignoreCommand>());
reg.registerCommand(std::make_unique<WhoCommand>());
reg.registerCommand(std::make_unique<DuelCommand>());
reg.registerCommand(std::make_unique<TradeCommand>());
}
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,195 @@
// 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_panel.hpp"
#include "ui/ui_services.hpp"
#include "game/game_handler.hpp"
#include "addons/addon_manager.hpp"
#include "core/application.hpp"
#include <algorithm>
#include <cctype>
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:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (ctx.args.empty()) return {false, false};
auto* am = ctx.services.addonManager;
if (am) {
am->runScript(ctx.args);
} else {
ctx.gameHandler.addUIError("Addon system not initialized.");
}
return {};
}
std::vector<std::string> aliases() const override { return {"run", "script"}; }
std::string helpText() const override { return "Execute Lua code"; }
};
// --- /dump, /print ---
class DumpCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (ctx.args.empty()) return {false, false};
auto* am = ctx.services.addonManager;
if (am && am->isInitialized()) {
// Wrap expression in print(tostring(...)) to display the value
std::string wrapped = "local __v = " + ctx.args +
"; 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.";
ctx.gameHandler.addLocalChatMessage(errMsg);
}
return {};
}
std::vector<std::string> aliases() const override { return {"dump", "print"}; }
std::string helpText() const override { return "Evaluate & print Lua expression"; }
};
// --- /reload, /reloadui, /rl ---
class ReloadCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
auto* am = ctx.services.addonManager;
if (am) {
am->reload();
am->fireEvent("VARIABLES_LOADED");
am->fireEvent("PLAYER_LOGIN");
am->fireEvent("PLAYER_ENTERING_WORLD");
game::MessageChatData rlMsg;
rlMsg.type = game::ChatType::SYSTEM;
rlMsg.language = game::ChatLanguage::UNIVERSAL;
rlMsg.message = "Interface reloaded.";
ctx.gameHandler.addLocalChatMessage(rlMsg);
} else {
game::MessageChatData rlMsg;
rlMsg.type = game::ChatType::SYSTEM;
rlMsg.language = game::ChatLanguage::UNIVERSAL;
rlMsg.message = "Addon system not available.";
ctx.gameHandler.addLocalChatMessage(rlMsg);
}
return {};
}
std::vector<std::string> aliases() const override { return {"reload", "reloadui", "rl"}; }
std::string helpText() const override { return "Reload all addons"; }
};
// --- /stopmacro [conditions] ---
class StopMacroCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
bool shouldStop = true;
if (!ctx.args.empty()) {
std::string condArg = ctx.args;
while (!condArg.empty() && condArg.front() == ' ') condArg.erase(condArg.begin());
if (!condArg.empty() && condArg.front() == '[') {
// Append a sentinel action so evaluateMacroConditionals can signal a match.
uint64_t tgtOver = static_cast<uint64_t>(-1);
std::string hit = evaluateMacroConditionals(condArg + " __stop__", ctx.gameHandler, tgtOver);
shouldStop = !hit.empty();
}
}
if (shouldStop) ctx.panel.macroStopped() = true;
return {};
}
std::vector<std::string> aliases() const override { return {"stopmacro"}; }
std::string helpText() const override { return "Stop macro execution"; }
};
// --- /clear ---
class ClearCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.clearChatHistory();
return {};
}
std::vector<std::string> aliases() const override { return {"clear"}; }
std::string helpText() const override { return "Clear chat history"; }
};
// --- /logout ---
class LogoutCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& /*ctx*/) override {
core::Application::getInstance().logoutToLogin();
return {};
}
std::vector<std::string> aliases() const override { return {"logout"}; }
std::string helpText() const override { return "Logout to login screen"; }
};
// --- /difficulty ---
class DifficultyCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
std::string arg = ctx.args;
// Trim whitespace
size_t first = arg.find_first_not_of(" \t");
size_t last = arg.find_last_not_of(" \t");
if (first != std::string::npos)
arg = arg.substr(first, last - first + 1);
else
arg.clear();
for (auto& ch : arg) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
uint32_t diff = 0;
bool valid = true;
if (arg == "normal" || arg == "0") diff = 0;
else if (arg == "heroic" || arg == "1") diff = 1;
else if (arg == "25" || arg == "25normal" || arg == "25man" || arg == "2")
diff = 2;
else if (arg == "25heroic" || arg == "25manheroic" || arg == "3")
diff = 3;
else valid = false;
if (!valid || arg.empty()) {
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "Usage: /difficulty normal|heroic|25|25heroic (0-3)";
ctx.gameHandler.addLocalChatMessage(msg);
} else {
static constexpr const char* kDiffNames[] = {
"Normal (5-man)", "Heroic (5-man)", "Normal (25-man)", "Heroic (25-man)"
};
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = std::string("Setting difficulty to: ") + kDiffNames[diff];
ctx.gameHandler.addLocalChatMessage(msg);
ctx.gameHandler.sendSetDifficulty(diff);
}
return {};
}
std::vector<std::string> aliases() const override { return {"difficulty"}; }
std::string helpText() const override { return "Set dungeon difficulty"; }
};
// --- Registration ---
void registerSystemCommands(ChatCommandRegistry& reg) {
reg.registerCommand(std::make_unique<RunCommand>());
reg.registerCommand(std::make_unique<DumpCommand>());
reg.registerCommand(std::make_unique<ReloadCommand>());
reg.registerCommand(std::make_unique<StopMacroCommand>());
reg.registerCommand(std::make_unique<ClearCommand>());
reg.registerCommand(std::make_unique<LogoutCommand>());
reg.registerCommand(std::make_unique<DifficultyCommand>());
}
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,252 @@
// Target commands: /target, /cleartarget, /targetenemy, /targetfriend,
// /targetlasttarget, /targetlastenemy, /targetlastfriend,
// /focus, /clearfocus
// Moved from ChatPanel::sendChatMessage() if/else chain (Phase 3).
#include "ui/chat/i_chat_command.hpp"
#include "ui/chat_panel.hpp"
#include "game/game_handler.hpp"
#include "game/entity.hpp"
#include <algorithm>
#include <cctype>
#include <limits>
#include <memory>
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.
inline void trimInPlace(std::string& s) {
while (!s.empty() && s.front() == ' ') s.erase(s.begin());
while (!s.empty() && s.back() == ' ') s.pop_back();
}
// Search nearby visible entities by name (case-insensitive prefix match).
// Returns the GUID of the nearest matching unit, or 0 if none found.
uint64_t findNearestByName(game::GameHandler& gh, const std::string& targetArgLower) {
uint64_t bestGuid = 0;
float bestDist = std::numeric_limits<float>::max();
const auto& pmi = gh.getMovementInfo();
for (const auto& [guid, entity] : gh.getEntityManager().getEntities()) {
if (!entity || entity->getType() == game::ObjectType::OBJECT) continue;
std::string name;
if (entity->getType() == game::ObjectType::PLAYER ||
entity->getType() == game::ObjectType::UNIT) {
auto unit = std::static_pointer_cast<game::Unit>(entity);
name = unit->getName();
}
if (name.empty()) continue;
std::string nameLower = name;
for (char& c : nameLower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (nameLower.find(targetArgLower) == 0) {
float dx = entity->getX() - pmi.x;
float dy = entity->getY() - pmi.y;
float dz = entity->getZ() - pmi.z;
float dist = dx*dx + dy*dy + dz*dz;
if (dist < bestDist) {
bestDist = dist;
bestGuid = guid;
}
}
}
return bestGuid;
}
} // anon namespace
// --- /target ---
class TargetCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (ctx.args.empty()) return {false, false};
std::string targetArg = ctx.args;
// Evaluate conditionals if present
uint64_t targetCmdOverride = static_cast<uint64_t>(-1);
if (!targetArg.empty() && targetArg.front() == '[') {
targetArg = evaluateMacroConditionals(targetArg, ctx.gameHandler, targetCmdOverride);
if (targetArg.empty() && targetCmdOverride == static_cast<uint64_t>(-1)) return {};
trimInPlace(targetArg);
}
// If conditionals resolved to a specific GUID, target it directly
if (targetCmdOverride != static_cast<uint64_t>(-1) && targetCmdOverride != 0) {
ctx.gameHandler.setTarget(targetCmdOverride);
return {};
}
if (targetArg.empty()) return {};
std::string targetArgLower = targetArg;
for (char& c : targetArgLower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
uint64_t bestGuid = findNearestByName(ctx.gameHandler, targetArgLower);
if (bestGuid) {
ctx.gameHandler.setTarget(bestGuid);
} else {
game::MessageChatData sysMsg;
sysMsg.type = game::ChatType::SYSTEM;
sysMsg.language = game::ChatLanguage::UNIVERSAL;
sysMsg.message = "No target matching '" + targetArg + "' found.";
ctx.gameHandler.addLocalChatMessage(sysMsg);
}
return {};
}
std::vector<std::string> aliases() const override { return {"target"}; }
std::string helpText() const override { return "Target unit by name"; }
};
// --- /cleartarget ---
class ClearTargetCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
bool condPass = true;
if (!ctx.args.empty()) {
std::string ctArg = ctx.args;
trimInPlace(ctArg);
if (!ctArg.empty() && ctArg.front() == '[') {
uint64_t ctOver = static_cast<uint64_t>(-1);
std::string res = evaluateMacroConditionals(ctArg, ctx.gameHandler, ctOver);
condPass = !(res.empty() && ctOver == static_cast<uint64_t>(-1));
}
}
if (condPass) ctx.gameHandler.clearTarget();
return {};
}
std::vector<std::string> aliases() const override { return {"cleartarget"}; }
std::string helpText() const override { return "Clear current target"; }
};
// --- /targetenemy ---
class TargetEnemyCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.targetEnemy(false);
return {};
}
std::vector<std::string> aliases() const override { return {"targetenemy"}; }
std::string helpText() const override { return "Cycle to next enemy"; }
};
// --- /targetfriend ---
class TargetFriendCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.targetFriend(false);
return {};
}
std::vector<std::string> aliases() const override { return {"targetfriend"}; }
std::string helpText() const override { return "Cycle to next friendly unit"; }
};
// --- /targetlasttarget, /targetlast ---
class TargetLastTargetCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.targetLastTarget();
return {};
}
std::vector<std::string> aliases() const override { return {"targetlasttarget", "targetlast"}; }
std::string helpText() const override { return "Target previous target"; }
};
// --- /targetlastenemy ---
class TargetLastEnemyCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.targetEnemy(true);
return {};
}
std::vector<std::string> aliases() const override { return {"targetlastenemy"}; }
std::string helpText() const override { return "Cycle to previous enemy"; }
};
// --- /targetlastfriend ---
class TargetLastFriendCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.targetFriend(true);
return {};
}
std::vector<std::string> aliases() const override { return {"targetlastfriend"}; }
std::string helpText() const override { return "Cycle to previous friendly unit"; }
};
// --- /focus ---
class FocusCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
if (!ctx.args.empty()) {
std::string focusArg = ctx.args;
// Evaluate conditionals if present
uint64_t focusCmdOverride = static_cast<uint64_t>(-1);
if (!focusArg.empty() && focusArg.front() == '[') {
focusArg = evaluateMacroConditionals(focusArg, ctx.gameHandler, focusCmdOverride);
if (focusArg.empty() && focusCmdOverride == static_cast<uint64_t>(-1)) return {};
trimInPlace(focusArg);
}
if (focusCmdOverride != static_cast<uint64_t>(-1) && focusCmdOverride != 0) {
ctx.gameHandler.setFocus(focusCmdOverride);
} else if (!focusArg.empty()) {
std::string focusArgLower = focusArg;
for (char& c : focusArgLower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
uint64_t bestGuid = findNearestByName(ctx.gameHandler, focusArgLower);
if (bestGuid) {
ctx.gameHandler.setFocus(bestGuid);
} else {
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "No unit matching '" + focusArg + "' found.";
ctx.gameHandler.addLocalChatMessage(msg);
}
}
} else if (ctx.gameHandler.hasTarget()) {
ctx.gameHandler.setFocus(ctx.gameHandler.getTargetGuid());
} else {
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "You must target a unit to set as focus.";
ctx.gameHandler.addLocalChatMessage(msg);
}
return {};
}
std::vector<std::string> aliases() const override { return {"focus"}; }
std::string helpText() const override { return "Set focus target"; }
};
// --- /clearfocus ---
class ClearFocusCommand : public IChatCommand {
public:
ChatCommandResult execute(ChatCommandContext& ctx) override {
ctx.gameHandler.clearFocus();
return {};
}
std::vector<std::string> aliases() const override { return {"clearfocus"}; }
std::string helpText() const override { return "Clear focus target"; }
};
// --- Registration ---
void registerTargetCommands(ChatCommandRegistry& reg) {
reg.registerCommand(std::make_unique<TargetCommand>());
reg.registerCommand(std::make_unique<ClearTargetCommand>());
reg.registerCommand(std::make_unique<TargetEnemyCommand>());
reg.registerCommand(std::make_unique<TargetFriendCommand>());
reg.registerCommand(std::make_unique<TargetLastTargetCommand>());
reg.registerCommand(std::make_unique<TargetLastEnemyCommand>());
reg.registerCommand(std::make_unique<TargetLastFriendCommand>());
reg.registerCommand(std::make_unique<FocusCommand>());
reg.registerCommand(std::make_unique<ClearFocusCommand>());
}
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,114 @@
// GameStateAdapter — concrete IGameState wrapping GameHandler + Renderer.
// Phase 4.2 of chat_panel_ref.md.
#include "ui/chat/game_state_adapter.hpp"
#include "game/game_handler.hpp"
#include "game/character.hpp"
#include "rendering/renderer.hpp"
#include <algorithm>
#include <cctype>
namespace wowee { namespace ui {
GameStateAdapter::GameStateAdapter(game::GameHandler& gameHandler,
rendering::Renderer* renderer)
: gameHandler_(gameHandler), renderer_(renderer) {}
// --- GUIDs ---
uint64_t GameStateAdapter::getPlayerGuid() const { return gameHandler_.getPlayerGuid(); }
uint64_t GameStateAdapter::getTargetGuid() const { return gameHandler_.getTargetGuid(); }
uint64_t GameStateAdapter::getFocusGuid() const { return gameHandler_.getFocusGuid(); }
uint64_t GameStateAdapter::getPetGuid() const { return gameHandler_.getPetGuid(); }
uint64_t GameStateAdapter::getMouseoverGuid() const { return gameHandler_.getMouseoverGuid(); }
// --- Player state ---
bool GameStateAdapter::isInCombat() const { return gameHandler_.isInCombat(); }
bool GameStateAdapter::isMounted() const { return gameHandler_.isMounted(); }
bool GameStateAdapter::isSwimming() const { return gameHandler_.isSwimming(); }
bool GameStateAdapter::isFlying() const { return gameHandler_.isPlayerFlying(); }
bool GameStateAdapter::isCasting() const { return gameHandler_.isCasting(); }
bool GameStateAdapter::isChanneling() const { return gameHandler_.isChanneling(); }
bool GameStateAdapter::isStealthed() const {
// Check UNIT_FLAG_SNEAKING (0x02000000) on player's Unit
auto pe = gameHandler_.getEntityManager().getEntity(gameHandler_.getPlayerGuid());
if (!pe) return false;
auto pu = std::dynamic_pointer_cast<game::Unit>(pe);
return pu && (pu->getUnitFlags() & 0x02000000u) != 0;
}
bool GameStateAdapter::hasPet() const { return gameHandler_.hasPet(); }
bool GameStateAdapter::isInGroup() const { return gameHandler_.isInGroup(); }
bool GameStateAdapter::isInRaid() const {
return gameHandler_.isInGroup() && gameHandler_.getPartyData().groupType == 1;
}
bool GameStateAdapter::isIndoors() const {
return renderer_ && renderer_->isPlayerIndoors();
}
// --- Numeric ---
uint8_t GameStateAdapter::getActiveTalentSpec() const { return gameHandler_.getActiveTalentSpec(); }
uint32_t GameStateAdapter::getVehicleId() const { return gameHandler_.getVehicleId(); }
uint32_t GameStateAdapter::getCurrentCastSpellId() const { return gameHandler_.getCurrentCastSpellId(); }
// --- Spell/aura ---
std::string GameStateAdapter::getSpellName(uint32_t spellId) const {
return gameHandler_.getSpellName(spellId);
}
bool GameStateAdapter::hasAuraByName(uint64_t targetGuid, const std::string& spellName,
bool wantDebuff) const {
// If targetGuid is player or invalid, check player auras; otherwise target auras
const std::vector<game::AuraSlot>* auras = nullptr;
uint64_t playerGuid = gameHandler_.getPlayerGuid();
if (targetGuid != static_cast<uint64_t>(-1) && targetGuid != 0 &&
targetGuid != playerGuid) {
auras = &gameHandler_.getTargetAuras();
} else {
auras = &gameHandler_.getPlayerAuras();
}
std::string nameLow = spellName;
for (char& ch : nameLow) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
for (const auto& a : *auras) {
if (a.isEmpty() || a.spellId == 0) continue;
bool isDebuff = (a.flags & 0x80) != 0;
if (wantDebuff ? !isDebuff : isDebuff) continue;
std::string sn = gameHandler_.getSpellName(a.spellId);
for (char& ch : sn) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
if (sn == nameLow) return true;
}
return false;
}
bool GameStateAdapter::hasFormAura() const {
for (const auto& a : gameHandler_.getPlayerAuras()) {
if (!a.isEmpty() && a.maxDurationMs == -1) return true;
}
return false;
}
// --- Entity queries ---
bool GameStateAdapter::entityExists(uint64_t guid) const {
if (guid == 0 || guid == static_cast<uint64_t>(-1)) return false;
return gameHandler_.getEntityManager().getEntity(guid) != nullptr;
}
bool GameStateAdapter::entityIsDead(uint64_t guid) const {
auto entity = gameHandler_.getEntityManager().getEntity(guid);
if (!entity) return false;
auto unit = std::dynamic_pointer_cast<game::Unit>(entity);
return unit && unit->getHealth() == 0;
}
bool GameStateAdapter::entityIsHostile(uint64_t guid) const {
auto entity = gameHandler_.getEntityManager().getEntity(guid);
if (!entity) return false;
auto unit = std::dynamic_pointer_cast<game::Unit>(entity);
return unit && gameHandler_.isHostileFactionPublic(unit->getFactionTemplate());
}
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,28 @@
// InputModifierAdapter — concrete IModifierState wrapping core::Input.
// Phase 4.3 of chat_panel_ref.md.
#include "ui/chat/input_modifier_adapter.hpp"
#include "core/input.hpp"
#include <SDL2/SDL_scancode.h>
namespace wowee { namespace ui {
bool InputModifierAdapter::isShiftHeld() const {
auto& input = core::Input::getInstance();
return input.isKeyPressed(SDL_SCANCODE_LSHIFT) ||
input.isKeyPressed(SDL_SCANCODE_RSHIFT);
}
bool InputModifierAdapter::isCtrlHeld() const {
auto& input = core::Input::getInstance();
return input.isKeyPressed(SDL_SCANCODE_LCTRL) ||
input.isKeyPressed(SDL_SCANCODE_RCTRL);
}
bool InputModifierAdapter::isAltHeld() const {
auto& input = core::Input::getInstance();
return input.isKeyPressed(SDL_SCANCODE_LALT) ||
input.isKeyPressed(SDL_SCANCODE_RALT);
}
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,223 @@
// MacroEvaluator — WoW macro conditional parser and evaluator.
// Moved from evaluateMacroConditionals() in chat_panel_commands.cpp (Phase 4.4).
#include "ui/chat/macro_evaluator.hpp"
#include "ui/chat/i_game_state.hpp"
#include "ui/chat/i_modifier_state.hpp"
#include <algorithm>
#include <cctype>
#include <vector>
namespace wowee { namespace ui {
MacroEvaluator::MacroEvaluator(IGameState& gameState, IModifierState& modState)
: gameState_(gameState), modState_(modState) {}
uint64_t MacroEvaluator::resolveEffectiveTarget(uint64_t tgt) const {
if (tgt != static_cast<uint64_t>(-1) && tgt != 0)
return tgt;
return gameState_.getTargetGuid();
}
bool MacroEvaluator::evalCondition(const std::string& raw, uint64_t& tgt) const {
// Trim
std::string c = raw;
size_t s = c.find_first_not_of(" \t");
c = (s != std::string::npos) ? c.substr(s) : "";
size_t e = c.find_last_not_of(" \t");
if (e != std::string::npos) c.resize(e + 1);
if (c.empty()) return true;
// --- @target specifiers ---
if (c[0] == '@') {
std::string spec = c.substr(1);
if (spec == "player") tgt = gameState_.getPlayerGuid();
else if (spec == "focus") tgt = gameState_.getFocusGuid();
else if (spec == "target") tgt = gameState_.getTargetGuid();
else if (spec == "pet") {
uint64_t pg = gameState_.getPetGuid();
if (pg != 0) tgt = pg; else return false;
}
else if (spec == "mouseover") {
uint64_t mo = gameState_.getMouseoverGuid();
if (mo != 0) tgt = mo; else return false;
}
return true;
}
// --- target=X specifiers ---
if (c.rfind("target=", 0) == 0) {
std::string spec = c.substr(7);
if (spec == "player") tgt = gameState_.getPlayerGuid();
else if (spec == "focus") tgt = gameState_.getFocusGuid();
else if (spec == "target") tgt = gameState_.getTargetGuid();
else if (spec == "pet") {
uint64_t pg = gameState_.getPetGuid();
if (pg != 0) tgt = pg; else return false;
}
else if (spec == "mouseover") {
uint64_t mo = gameState_.getMouseoverGuid();
if (mo != 0) tgt = mo; else return false;
}
return true;
}
// --- Modifier keys ---
const bool shiftHeld = modState_.isShiftHeld();
const bool ctrlHeld = modState_.isCtrlHeld();
const bool altHeld = modState_.isAltHeld();
const bool anyMod = shiftHeld || ctrlHeld || altHeld;
if (c == "nomod" || c == "mod:none") return !anyMod;
if (c.rfind("mod:", 0) == 0) {
std::string mods = c.substr(4);
bool ok = true;
if (mods.find("shift") != std::string::npos && !shiftHeld) ok = false;
if (mods.find("ctrl") != std::string::npos && !ctrlHeld) ok = false;
if (mods.find("alt") != std::string::npos && !altHeld) ok = false;
return ok;
}
// --- Combat ---
if (c == "combat") return gameState_.isInCombat();
if (c == "nocombat") return !gameState_.isInCombat();
// --- Effective target for exists/dead/help/harm ---
uint64_t eff = resolveEffectiveTarget(tgt);
if (c == "exists") return gameState_.entityExists(eff);
if (c == "noexists") return !gameState_.entityExists(eff);
if (c == "dead") return gameState_.entityIsDead(eff);
if (c == "nodead") return !gameState_.entityIsDead(eff);
if (c == "harm" || c == "nohelp") return gameState_.entityIsHostile(eff);
if (c == "help" || c == "noharm") return !gameState_.entityIsHostile(eff);
// --- Mounted / swimming / flying ---
if (c == "mounted") return gameState_.isMounted();
if (c == "nomounted") return !gameState_.isMounted();
if (c == "swimming") return gameState_.isSwimming();
if (c == "noswimming") return !gameState_.isSwimming();
if (c == "flying") return gameState_.isFlying();
if (c == "noflying") return !gameState_.isFlying();
// --- Channeling / casting ---
if (c == "channeling") return gameState_.isCasting() && gameState_.isChanneling();
if (c == "nochanneling") return !(gameState_.isCasting() && gameState_.isChanneling());
if (c.rfind("channeling:", 0) == 0 && c.size() > 11) {
if (!gameState_.isChanneling()) return false;
std::string want = c.substr(11);
for (char& ch : want) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
uint32_t castSpellId = gameState_.getCurrentCastSpellId();
std::string sn = gameState_.getSpellName(castSpellId);
for (char& ch : sn) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
return sn == want;
}
if (c == "casting") return gameState_.isCasting();
if (c == "nocasting") return !gameState_.isCasting();
// --- Stealthed ---
if (c == "stealthed") return gameState_.isStealthed();
if (c == "nostealthed") return !gameState_.isStealthed();
// --- Pet ---
if (c == "pet") return gameState_.hasPet();
if (c == "nopet") return !gameState_.hasPet();
// --- Indoors / outdoors ---
if (c == "indoors" || c == "nooutdoors") return gameState_.isIndoors();
if (c == "outdoors" || c == "noindoors") return !gameState_.isIndoors();
// --- Group / raid ---
if (c == "group" || c == "party") return gameState_.isInGroup();
if (c == "nogroup") return !gameState_.isInGroup();
if (c == "raid") return gameState_.isInRaid();
if (c == "noraid") return !gameState_.isInRaid();
// --- Talent spec ---
if (c.rfind("spec:", 0) == 0) {
uint8_t wantSpec = 0;
try { wantSpec = static_cast<uint8_t>(std::stoul(c.substr(5))); } catch (...) {}
return wantSpec > 0 && gameState_.getActiveTalentSpec() == (wantSpec - 1);
}
// --- Form / stance ---
if (c == "noform" || c == "nostance" || c == "form:0" || c == "stance:0")
return !gameState_.hasFormAura();
// --- Buff / debuff ---
if (c.rfind("buff:", 0) == 0 && c.size() > 5)
return gameState_.hasAuraByName(tgt, c.substr(5), false);
if (c.rfind("nobuff:", 0) == 0 && c.size() > 7)
return !gameState_.hasAuraByName(tgt, c.substr(7), false);
if (c.rfind("debuff:", 0) == 0 && c.size() > 7)
return gameState_.hasAuraByName(tgt, c.substr(7), true);
if (c.rfind("nodebuff:", 0) == 0 && c.size() > 9)
return !gameState_.hasAuraByName(tgt, c.substr(9), true);
// --- Vehicle ---
if (c == "vehicle") return gameState_.getVehicleId() != 0;
if (c == "novehicle") return gameState_.getVehicleId() == 0;
// Unknown → permissive (don't block)
return true;
}
std::string MacroEvaluator::evaluate(const std::string& rawArg,
uint64_t& targetOverride) const {
targetOverride = static_cast<uint64_t>(-1);
// Split rawArg on ';' → alternatives
std::vector<std::string> alts;
{
std::string cur;
for (char ch : rawArg) {
if (ch == ';') { alts.push_back(cur); cur.clear(); }
else cur += ch;
}
alts.push_back(cur);
}
for (auto& alt : alts) {
// Trim
size_t fs = alt.find_first_not_of(" \t");
if (fs == std::string::npos) continue;
alt = alt.substr(fs);
size_t ls = alt.find_last_not_of(" \t");
if (ls != std::string::npos) alt.resize(ls + 1);
if (!alt.empty() && alt[0] == '[') {
size_t close = alt.find(']');
if (close == std::string::npos) continue;
std::string condStr = alt.substr(1, close - 1);
std::string argPart = alt.substr(close + 1);
// Trim argPart
size_t as = argPart.find_first_not_of(" \t");
argPart = (as != std::string::npos) ? argPart.substr(as) : "";
// Evaluate comma-separated conditions
uint64_t tgt = static_cast<uint64_t>(-1);
bool pass = true;
size_t cp = 0;
while (pass) {
size_t comma = condStr.find(',', cp);
std::string tok = condStr.substr(cp,
comma == std::string::npos ? std::string::npos : comma - cp);
if (!evalCondition(tok, tgt)) { pass = false; break; }
if (comma == std::string::npos) break;
cp = comma + 1;
}
if (pass) {
if (tgt != static_cast<uint64_t>(-1)) targetOverride = tgt;
return argPart;
}
} else {
// No condition block — default fallback always matches
return alt;
}
}
return {};
}
} // namespace ui
} // namespace wowee

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -31,106 +31,7 @@ namespace {
namespace wowee { namespace ui {
const char* ChatPanel::getChatTypeName(game::ChatType type) const {
switch (type) {
case game::ChatType::SAY: return "Say";
case game::ChatType::YELL: return "Yell";
case game::ChatType::EMOTE: return "Emote";
case game::ChatType::TEXT_EMOTE: return "Emote";
case game::ChatType::PARTY: return "Party";
case game::ChatType::GUILD: return "Guild";
case game::ChatType::OFFICER: return "Officer";
case game::ChatType::RAID: return "Raid";
case game::ChatType::RAID_LEADER: return "Raid Leader";
case game::ChatType::RAID_WARNING: return "Raid Warning";
case game::ChatType::BATTLEGROUND: return "Battleground";
case game::ChatType::BATTLEGROUND_LEADER: return "Battleground Leader";
case game::ChatType::WHISPER: return "Whisper";
case game::ChatType::WHISPER_INFORM: return "To";
case game::ChatType::SYSTEM: return "System";
case game::ChatType::MONSTER_SAY: return "Say";
case game::ChatType::MONSTER_YELL: return "Yell";
case game::ChatType::MONSTER_EMOTE: return "Emote";
case game::ChatType::CHANNEL: return "Channel";
case game::ChatType::ACHIEVEMENT: return "Achievement";
case game::ChatType::DND: return "DND";
case game::ChatType::AFK: return "AFK";
case game::ChatType::BG_SYSTEM_NEUTRAL:
case game::ChatType::BG_SYSTEM_ALLIANCE:
case game::ChatType::BG_SYSTEM_HORDE: return "System";
default: return "Unknown";
}
}
ImVec4 ChatPanel::getChatTypeColor(game::ChatType type) const {
switch (type) {
case game::ChatType::SAY:
return ui::colors::kWhite; // White
case game::ChatType::YELL:
return kColorRed; // Red
case game::ChatType::EMOTE:
return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange
case game::ChatType::TEXT_EMOTE:
return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange
case game::ChatType::PARTY:
return ImVec4(0.5f, 0.5f, 1.0f, 1.0f); // Light blue
case game::ChatType::GUILD:
return kColorBrightGreen; // Green
case game::ChatType::OFFICER:
return ImVec4(0.3f, 0.8f, 0.3f, 1.0f); // Dark green
case game::ChatType::RAID:
return ImVec4(1.0f, 0.5f, 0.0f, 1.0f); // Orange
case game::ChatType::RAID_LEADER:
return ImVec4(1.0f, 0.4f, 0.0f, 1.0f); // Darker orange
case game::ChatType::RAID_WARNING:
return ImVec4(1.0f, 0.0f, 0.0f, 1.0f); // Red
case game::ChatType::BATTLEGROUND:
return ImVec4(1.0f, 0.6f, 0.0f, 1.0f); // Orange-gold
case game::ChatType::BATTLEGROUND_LEADER:
return ImVec4(1.0f, 0.5f, 0.0f, 1.0f); // Orange
case game::ChatType::WHISPER:
return ImVec4(1.0f, 0.5f, 1.0f, 1.0f); // Pink
case game::ChatType::WHISPER_INFORM:
return ImVec4(1.0f, 0.5f, 1.0f, 1.0f); // Pink
case game::ChatType::SYSTEM:
return kColorYellow; // Yellow
case game::ChatType::MONSTER_SAY:
return ui::colors::kWhite; // White (same as SAY)
case game::ChatType::MONSTER_YELL:
return kColorRed; // Red (same as YELL)
case game::ChatType::MONSTER_EMOTE:
return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange (same as EMOTE)
case game::ChatType::CHANNEL:
return ImVec4(1.0f, 0.7f, 0.7f, 1.0f); // Light pink
case game::ChatType::ACHIEVEMENT:
return ImVec4(1.0f, 1.0f, 0.0f, 1.0f); // Bright yellow
case game::ChatType::GUILD_ACHIEVEMENT:
return colors::kWarmGold; // Gold
case game::ChatType::SKILL:
return colors::kCyan; // Cyan
case game::ChatType::LOOT:
return ImVec4(0.8f, 0.5f, 1.0f, 1.0f); // Light purple
case game::ChatType::MONSTER_WHISPER:
case game::ChatType::RAID_BOSS_WHISPER:
return ImVec4(1.0f, 0.5f, 1.0f, 1.0f); // Pink (same as WHISPER)
case game::ChatType::RAID_BOSS_EMOTE:
return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange (same as EMOTE)
case game::ChatType::MONSTER_PARTY:
return ImVec4(0.5f, 0.5f, 1.0f, 1.0f); // Light blue (same as PARTY)
case game::ChatType::BG_SYSTEM_NEUTRAL:
return colors::kWarmGold; // Gold
case game::ChatType::BG_SYSTEM_ALLIANCE:
return ImVec4(0.3f, 0.6f, 1.0f, 1.0f); // Blue
case game::ChatType::BG_SYSTEM_HORDE:
return kColorRed; // Red
case game::ChatType::AFK:
case game::ChatType::DND:
return ImVec4(0.85f, 0.85f, 0.85f, 0.8f); // Light gray
default:
return ui::colors::kLightGray; // Gray
}
}
// getChatTypeName / getChatTypeColor moved to ChatTabManager (Phase 1.3)
std::string ChatPanel::replaceGenderPlaceholders(const std::string& text, game::GameHandler& gameHandler) {
@ -276,116 +177,17 @@ std::string ChatPanel::replaceGenderPlaceholders(const std::string& text, game::
return result;
}
// renderBubbles delegates to ChatBubbleManager (Phase 1.4)
void ChatPanel::renderBubbles(game::GameHandler& gameHandler) {
if (chatBubbles_.empty()) return;
auto* renderer = services_.renderer;
auto* camera = renderer ? renderer->getCamera() : nullptr;
if (!camera) return;
auto* window = services_.window;
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
// Get delta time from ImGui
float dt = ImGui::GetIO().DeltaTime;
glm::mat4 viewProj = camera->getProjectionMatrix() * camera->getViewMatrix();
// Update and render bubbles
for (int i = static_cast<int>(chatBubbles_.size()) - 1; i >= 0; --i) {
auto& bubble = chatBubbles_[i];
bubble.timeRemaining -= dt;
if (bubble.timeRemaining <= 0.0f) {
chatBubbles_.erase(chatBubbles_.begin() + i);
continue;
}
// Get entity position
auto entity = gameHandler.getEntityManager().getEntity(bubble.senderGuid);
if (!entity) continue;
// Convert canonical → render coordinates, offset up by 2.5 units for bubble above head
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ() + 2.5f);
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
// Project to screen
glm::vec4 clipPos = viewProj * glm::vec4(renderPos, 1.0f);
if (clipPos.w <= 0.0f) continue; // Behind camera
glm::vec2 ndc(clipPos.x / clipPos.w, clipPos.y / clipPos.w);
float screenX = (ndc.x * 0.5f + 0.5f) * screenW;
// Camera bakes the Vulkan Y-flip into the projection matrix:
// NDC y=-1 is top, y=1 is bottom — same convention as nameplate/minimap projection.
float screenY = (ndc.y * 0.5f + 0.5f) * screenH;
// Skip if off-screen
if (screenX < -200.0f || screenX > screenW + 200.0f ||
screenY < -100.0f || screenY > screenH + 100.0f) continue;
// Fade alpha over last 2 seconds
float alpha = 1.0f;
if (bubble.timeRemaining < 2.0f) {
alpha = bubble.timeRemaining / 2.0f;
}
// Draw bubble window
std::string winId = "##ChatBubble" + std::to_string(bubble.senderGuid);
ImGui::SetNextWindowPos(ImVec2(screenX, screenY), ImGuiCond_Always, ImVec2(0.5f, 1.0f));
ImGui::SetNextWindowBgAlpha(0.7f * alpha);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoInputs |
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav;
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 4));
ImGui::Begin(winId.c_str(), nullptr, flags);
ImVec4 textColor = bubble.isYell
? ImVec4(1.0f, 0.2f, 0.2f, alpha)
: ImVec4(1.0f, 1.0f, 1.0f, alpha);
ImGui::PushStyleColor(ImGuiCol_Text, textColor);
ImGui::PushTextWrapPos(200.0f);
ImGui::TextWrapped("%s", bubble.message.c_str());
ImGui::PopTextWrapPos();
ImGui::PopStyleColor();
ImGui::End();
ImGui::PopStyleVar(2);
}
bubbleManager_.render(gameHandler, services_);
}
// ---- Public interface methods ----
// setupCallbacks delegates to ChatBubbleManager (Phase 1.4)
void ChatPanel::setupCallbacks(game::GameHandler& gameHandler) {
if (!chatBubbleCallbackSet_) {
gameHandler.setChatBubbleCallback([this](uint64_t guid, const std::string& msg, bool isYell) {
float duration = 8.0f + static_cast<float>(msg.size()) * 0.06f;
if (isYell) duration += 2.0f;
if (duration > 15.0f) duration = 15.0f;
// Replace existing bubble for same sender
for (auto& b : chatBubbles_) {
if (b.senderGuid == guid) {
b.message = msg;
b.timeRemaining = duration;
b.totalDuration = duration;
b.isYell = isYell;
return;
}
}
// Evict oldest if too many
if (chatBubbles_.size() >= 10) {
chatBubbles_.erase(chatBubbles_.begin());
}
chatBubbles_.push_back({guid, msg, duration, duration, isYell});
});
chatBubbleCallbackSet_ = true;
}
bubbleManager_.setupCallback(gameHandler);
}
void ChatPanel::insertChatLink(const std::string& link) {
@ -406,6 +208,7 @@ void ChatPanel::activateSlashInput() {
}
void ChatPanel::activateInput() {
if (chatInputCooldown_ > 0) return; // suppress re-activation right after send
refocusChatInput_ = true;
}
@ -422,59 +225,5 @@ ChatPanel::SlashCommands ChatPanel::consumeSlashCommands() {
return result;
}
void ChatPanel::renderSettingsTab(std::function<void()> saveSettingsFn) {
ImGui::Spacing();
ImGui::Text("Appearance");
ImGui::Separator();
if (ImGui::Checkbox("Show Timestamps", &chatShowTimestamps)) {
saveSettingsFn();
}
ImGui::SetItemTooltip("Show [HH:MM] before each chat message");
const char* fontSizes[] = { "Small", "Medium", "Large" };
if (ImGui::Combo("Chat Font Size", &chatFontSize, fontSizes, 3)) {
saveSettingsFn();
}
ImGui::Spacing();
ImGui::Spacing();
ImGui::Text("Auto-Join Channels");
ImGui::Separator();
if (ImGui::Checkbox("General", &chatAutoJoinGeneral)) saveSettingsFn();
if (ImGui::Checkbox("Trade", &chatAutoJoinTrade)) saveSettingsFn();
if (ImGui::Checkbox("LocalDefense", &chatAutoJoinLocalDefense)) saveSettingsFn();
if (ImGui::Checkbox("LookingForGroup", &chatAutoJoinLFG)) saveSettingsFn();
if (ImGui::Checkbox("Local", &chatAutoJoinLocal)) saveSettingsFn();
ImGui::Spacing();
ImGui::Spacing();
ImGui::Text("Joined Channels");
ImGui::Separator();
ImGui::TextDisabled("Use /join and /leave commands in chat to manage channels.");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
if (ImGui::Button("Restore Chat Defaults", ImVec2(-1, 0))) {
restoreDefaults();
saveSettingsFn();
}
}
void ChatPanel::restoreDefaults() {
chatShowTimestamps = false;
chatFontSize = 1;
chatAutoJoinGeneral = true;
chatAutoJoinTrade = true;
chatAutoJoinLocalDefense = true;
chatAutoJoinLFG = true;
chatAutoJoinLocal = true;
}
} // namespace ui
} // namespace wowee

View file

@ -895,15 +895,16 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
chatPanel_.activateSlashInput();
}
if (!io.WantTextInput && !chatPanel_.isChatInputActive() &&
KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHAT, true)) {
KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHAT, false)) {
chatPanel_.activateInput();
}
const bool textFocus = chatPanel_.isChatInputActive() || io.WantTextInput;
// Tab targeting (when keyboard not captured by UI)
if (!io.WantCaptureKeyboard) {
// When typing in chat (or any text input), never treat keys as gameplay/UI shortcuts.
// 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);
@ -1005,7 +1006,7 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
}
// Toggle Titles window with H (hero/title screen — no conflicting keybinding)
if (input.isKeyJustPressed(SDL_SCANCODE_H) && !ImGui::GetIO().WantCaptureKeyboard) {
if (input.isKeyJustPressed(SDL_SCANCODE_H)) {
windowManager_.showTitlesWindow_ = !windowManager_.showTitlesWindow_;
}
@ -1065,7 +1066,7 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
} else if (bar[slotIdx].type == game::ActionBarSlot::ITEM && bar[slotIdx].id != 0) {
gameHandler.useItemById(bar[slotIdx].id);
} else if (bar[slotIdx].type == game::ActionBarSlot::MACRO) {
chatPanel_.executeMacroText(gameHandler, inventoryScreen, spellbookScreen, questLogScreen, gameHandler.getMacroText(bar[slotIdx].id));
chatPanel_.executeMacroText(gameHandler, gameHandler.getMacroText(bar[slotIdx].id));
}
}
}