mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-04 16:23:52 +00:00
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:
parent
09c4a9a04a
commit
42f1bb98ea
54 changed files with 7363 additions and 3856 deletions
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
128
src/ui/chat/chat_bubble_manager.cpp
Normal file
128
src/ui/chat/chat_bubble_manager.cpp
Normal 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
|
||||
55
src/ui/chat/chat_command_registry.cpp
Normal file
55
src/ui/chat/chat_command_registry.cpp
Normal 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
|
||||
68
src/ui/chat/chat_input.cpp
Normal file
68
src/ui/chat/chat_input.cpp
Normal 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
|
||||
206
src/ui/chat/chat_markup_parser.cpp
Normal file
206
src/ui/chat/chat_markup_parser.cpp
Normal 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
|
||||
704
src/ui/chat/chat_markup_renderer.cpp
Normal file
704
src/ui/chat/chat_markup_renderer.cpp
Normal 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
|
||||
63
src/ui/chat/chat_settings.cpp
Normal file
63
src/ui/chat/chat_settings.cpp
Normal 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
|
||||
35
src/ui/chat/chat_tab_completer.cpp
Normal file
35
src/ui/chat/chat_tab_completer.cpp
Normal 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
|
||||
198
src/ui/chat/chat_tab_manager.cpp
Normal file
198
src/ui/chat/chat_tab_manager.cpp
Normal 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
|
||||
341
src/ui/chat/commands/channel_commands.cpp
Normal file
341
src/ui/chat/commands/channel_commands.cpp
Normal 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
|
||||
543
src/ui/chat/commands/combat_commands.cpp
Normal file
543
src/ui/chat/commands/combat_commands.cpp
Normal 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
|
||||
230
src/ui/chat/commands/emote_commands.cpp
Normal file
230
src/ui/chat/commands/emote_commands.cpp
Normal 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
|
||||
127
src/ui/chat/commands/gm_commands.cpp
Normal file
127
src/ui/chat/commands/gm_commands.cpp
Normal 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
|
||||
395
src/ui/chat/commands/group_commands.cpp
Normal file
395
src/ui/chat/commands/group_commands.cpp
Normal 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
|
||||
203
src/ui/chat/commands/guild_commands.cpp
Normal file
203
src/ui/chat/commands/guild_commands.cpp
Normal 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
|
||||
124
src/ui/chat/commands/help_commands.cpp
Normal file
124
src/ui/chat/commands/help_commands.cpp
Normal 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
|
||||
404
src/ui/chat/commands/misc_commands.cpp
Normal file
404
src/ui/chat/commands/misc_commands.cpp
Normal 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
|
||||
211
src/ui/chat/commands/social_commands.cpp
Normal file
211
src/ui/chat/commands/social_commands.cpp
Normal 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
|
||||
195
src/ui/chat/commands/system_commands.cpp
Normal file
195
src/ui/chat/commands/system_commands.cpp
Normal 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
|
||||
252
src/ui/chat/commands/target_commands.cpp
Normal file
252
src/ui/chat/commands/target_commands.cpp
Normal 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
|
||||
114
src/ui/chat/game_state_adapter.cpp
Normal file
114
src/ui/chat/game_state_adapter.cpp
Normal 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
|
||||
28
src/ui/chat/input_modifier_adapter.cpp
Normal file
28
src/ui/chat/input_modifier_adapter.cpp
Normal 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
|
||||
223
src/ui/chat/macro_evaluator.cpp
Normal file
223
src/ui/chat/macro_evaluator.cpp
Normal 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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue