Merge pull request #62 from ldmonster/chore/chat-system

[chore] refactor(chat): decompose ChatPanel into modular architecture
This commit is contained in:
Kelsi Rae Davis 2026-04-13 21:50:32 -07:00 committed by GitHub
commit 3c8736449a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 8089 additions and 4743 deletions

View file

@ -685,8 +685,31 @@ set(WOWEE_SOURCES
src/ui/game_screen_hud.cpp
src/ui/game_screen_minimap.cpp
src/ui/chat_panel.cpp
src/ui/chat_panel_commands.cpp
src/ui/chat_panel_utils.cpp
src/ui/chat/chat_settings.cpp
src/ui/chat/chat_input.cpp
src/ui/chat/chat_utils.cpp
src/ui/chat/chat_tab_manager.cpp
src/ui/chat/chat_bubble_manager.cpp
src/ui/chat/chat_markup_parser.cpp
src/ui/chat/chat_markup_renderer.cpp
src/ui/chat/item_tooltip_renderer.cpp
src/ui/chat/chat_command_registry.cpp
src/ui/chat/chat_tab_completer.cpp
src/ui/chat/macro_evaluator.cpp
src/ui/chat/macro_eval_convenience.cpp
src/ui/chat/game_state_adapter.cpp
src/ui/chat/input_modifier_adapter.cpp
src/ui/chat/commands/system_commands.cpp
src/ui/chat/commands/social_commands.cpp
src/ui/chat/commands/channel_commands.cpp
src/ui/chat/commands/combat_commands.cpp
src/ui/chat/commands/group_commands.cpp
src/ui/chat/commands/guild_commands.cpp
src/ui/chat/commands/target_commands.cpp
src/ui/chat/commands/emote_commands.cpp
src/ui/chat/commands/misc_commands.cpp
src/ui/chat/commands/help_commands.cpp
src/ui/chat/commands/gm_commands.cpp
src/ui/toast_manager.cpp
src/ui/dialog_manager.cpp
src/ui/settings_panel.cpp

View file

@ -570,48 +570,52 @@ public:
};
/**
* Chat message types
* Chat message types wire values shared across Vanilla 1.12, TBC 2.4.3, and WotLK 3.3.5a.
* Core types (0x000x1B) are identical in all expansions.
* WotLK adds: ACHIEVEMENT(0x30), GUILD_ACHIEVEMENT(0x31), PARTY_LEADER(0x33).
*/
enum class ChatType : uint8_t {
SAY = 0,
PARTY = 1,
RAID = 2,
GUILD = 3,
OFFICER = 4,
YELL = 5,
WHISPER = 6,
WHISPER_INFORM = 7,
EMOTE = 8,
TEXT_EMOTE = 9,
SYSTEM = 10,
MONSTER_SAY = 11,
MONSTER_YELL = 12,
MONSTER_EMOTE = 13,
CHANNEL = 14,
CHANNEL_JOIN = 15,
CHANNEL_LEAVE = 16,
CHANNEL_LIST = 17,
CHANNEL_NOTICE = 18,
CHANNEL_NOTICE_USER = 19,
AFK = 20,
DND = 21,
IGNORED = 22,
SKILL = 23,
LOOT = 24,
BATTLEGROUND = 25,
BATTLEGROUND_LEADER = 26,
RAID_LEADER = 27,
RAID_WARNING = 28,
ACHIEVEMENT = 29,
GUILD_ACHIEVEMENT = 30,
MONSTER_WHISPER = 42,
RAID_BOSS_WHISPER = 43,
RAID_BOSS_EMOTE = 44,
MONSTER_PARTY = 50,
// BG/Arena system messages (WoW 3.3.5a — no sender, treated as SYSTEM in display)
BG_SYSTEM_NEUTRAL = 82,
BG_SYSTEM_ALLIANCE = 83,
BG_SYSTEM_HORDE = 84
SYSTEM = 0x00,
SAY = 0x01,
PARTY = 0x02,
RAID = 0x03,
GUILD = 0x04,
OFFICER = 0x05,
YELL = 0x06,
WHISPER = 0x07,
WHISPER_FOREIGN = 0x08,
WHISPER_INFORM = 0x09,
EMOTE = 0x0A,
TEXT_EMOTE = 0x0B,
MONSTER_SAY = 0x0C,
MONSTER_PARTY = 0x0D,
MONSTER_YELL = 0x0E,
MONSTER_WHISPER = 0x0F,
MONSTER_EMOTE = 0x10,
CHANNEL = 0x11,
CHANNEL_JOIN = 0x12,
CHANNEL_LEAVE = 0x13,
CHANNEL_LIST = 0x14,
CHANNEL_NOTICE = 0x15,
CHANNEL_NOTICE_USER = 0x16,
AFK = 0x17,
DND = 0x18,
IGNORED = 0x19,
SKILL = 0x1A,
LOOT = 0x1B,
// 0x240x26: BG system messages
BG_SYSTEM_NEUTRAL = 0x24,
BG_SYSTEM_ALLIANCE = 0x25,
BG_SYSTEM_HORDE = 0x26,
RAID_LEADER = 0x27,
RAID_WARNING = 0x28,
RAID_BOSS_EMOTE = 0x29,
RAID_BOSS_WHISPER = 0x2A,
BATTLEGROUND = 0x2C,
BATTLEGROUND_LEADER = 0x2D,
ACHIEVEMENT = 0x30,
GUILD_ACHIEVEMENT = 0x31,
PARTY_LEADER = 0x33,
};
/**

View file

@ -0,0 +1,37 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <unordered_map>
namespace wowee {
namespace ui {
/**
* /castsequence persistent state shared across all macros using the same spell list.
*
* Extracted from chat_panel_commands.cpp static global (Phase 1.5 of chat_panel_ref.md).
* Keyed by the normalized (lowercase, comma-joined) spell sequence string.
*/
class CastSequenceTracker {
public:
struct State {
size_t index = 0;
float lastPressSec = 0.0f;
uint64_t lastTargetGuid = 0;
bool lastInCombat = false;
};
/** Get (or create) the state for a given sequence key. */
State& get(const std::string& seqKey) { return states_[seqKey]; }
/** Reset all tracked sequences. */
void clear() { states_.clear(); }
private:
std::unordered_map<std::string, State> states_;
};
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,46 @@
#pragma once
#include "ui/ui_services.hpp"
#include <cstdint>
#include <string>
#include <vector>
namespace wowee {
namespace game { class GameHandler; }
namespace ui {
/**
* Manages 3D-projected chat bubbles above entities.
*
* Extracted from ChatPanel (Phase 1.4 of chat_panel_ref.md).
* Owns bubble lifecycle: add, update (tick), render (ImGui overlay).
*/
class ChatBubbleManager {
public:
/** Add or replace a bubble for the given entity. */
void addBubble(uint64_t senderGuid, const std::string& message, bool isYell);
/** Render and tick all active bubbles (projects to screen via camera). */
void render(game::GameHandler& gameHandler, const UIServices& services);
/** Register the chat-bubble callback on GameHandler (call once per session). */
void setupCallback(game::GameHandler& gameHandler);
bool empty() const { return bubbles_.empty(); }
private:
struct ChatBubble {
uint64_t senderGuid = 0;
std::string message;
float timeRemaining = 0.0f;
float totalDuration = 0.0f;
bool isYell = false;
};
std::vector<ChatBubble> bubbles_;
bool callbackSet_ = false;
static constexpr size_t kMaxBubbles = 10;
};
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,50 @@
// ChatCommandRegistry — command registration + dispatch.
// Replaces the 500-line if/else chain in sendChatMessage() (Phase 3.1).
#pragma once
#include "ui/chat/i_chat_command.hpp"
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
namespace wowee {
namespace ui {
/**
* Registry of all slash commands.
*
* dispatch() looks up the command by alias and calls execute().
* getCompletions() provides tab-completion for /command prefixes.
*/
class ChatCommandRegistry {
public:
/** Register a command (takes ownership). All aliases are mapped. */
void registerCommand(std::unique_ptr<IChatCommand> cmd);
/**
* Dispatch a slash command.
* @param cmdLower lowercase command name (e.g. "cast", "whisper")
* @param ctx context with args, gameHandler, services, etc.
* @return result indicating if handled and whether to clear input
*/
ChatCommandResult dispatch(const std::string& cmdLower, ChatCommandContext& ctx);
/** Get all command aliases matching a prefix (for tab completion). */
std::vector<std::string> getCompletions(const std::string& prefix) const;
/** Get help entries: (alias, helpText) for all registered commands. */
std::vector<std::pair<std::string, std::string>> getHelpEntries() const;
/** Check if a command alias is registered. */
bool hasCommand(const std::string& alias) const;
private:
// alias → raw pointer (non-owning, commands_ owns the objects)
std::unordered_map<std::string, IChatCommand*> commandMap_;
// Ownership of all registered commands
std::vector<std::unique_ptr<IChatCommand>> commands_;
};
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,37 @@
// Forward declarations for the chat subsystem.
// Include this instead of the full headers when only pointers/references are needed.
// Extracted in Phase 6.6 of chat_panel_ref.md.
#pragma once
namespace wowee {
namespace game { class GameHandler; }
namespace ui {
class ChatPanel;
class InventoryScreen;
class SpellbookScreen;
class QuestLogScreen;
// Chat subsystem types (under include/ui/chat/)
class ChatSettings;
class ChatInput;
class ChatTabManager;
class ChatBubbleManager;
class ChatMarkupParser;
class ChatMarkupRenderer;
class ChatCommandRegistry;
class ChatTabCompleter;
class CastSequenceTracker;
class MacroEvaluator;
class IGameState;
class IModifierState;
class IChatCommand;
struct ChatCommandContext;
struct ChatCommandResult;
struct UIServices;
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,95 @@
#pragma once
#include <cstring>
#include <string>
#include <vector>
namespace wowee {
namespace ui {
/**
* Chat input state: buffer, whisper target, sent-history, focus management.
*
* Extracted from ChatPanel (Phase 1.2 of chat_panel_ref.md).
* No UI or network dependencies pure state management.
*/
class ChatInput {
public:
// ---- Buffer access ----
char* getBuffer() { return buffer_; }
const char* getBuffer() const { return buffer_; }
size_t getBufferSize() const { return sizeof(buffer_); }
char* getWhisperBuffer() { return whisperBuffer_; }
const char* getWhisperBuffer() const { return whisperBuffer_; }
size_t getWhisperBufferSize() const { return sizeof(whisperBuffer_); }
void clear() { buffer_[0] = '\0'; }
bool isEmpty() const { return buffer_[0] == '\0'; }
std::string getText() const { return std::string(buffer_); }
void setText(const std::string& text) {
strncpy(buffer_, text.c_str(), sizeof(buffer_) - 1);
buffer_[sizeof(buffer_) - 1] = '\0';
}
// ---- Whisper target ----
std::string getWhisperTarget() const { return std::string(whisperBuffer_); }
void setWhisperTarget(const std::string& name) {
strncpy(whisperBuffer_, name.c_str(), sizeof(whisperBuffer_) - 1);
whisperBuffer_[sizeof(whisperBuffer_) - 1] = '\0';
}
// ---- Sent-message history (Up/Down arrow recall) ----
void pushToHistory(const std::string& msg);
std::string historyUp();
std::string historyDown();
void resetHistoryIndex() { historyIdx_ = -1; }
int getHistoryIndex() const { return historyIdx_; }
const std::vector<std::string>& getSentHistory() const { return sentHistory_; }
// ---- Focus state ----
bool isActive() const { return active_; }
void setActive(bool v) { active_ = v; }
bool shouldFocus() const { return focusRequested_; }
void requestFocus() { focusRequested_ = true; }
void clearFocusRequest() { focusRequested_ = false; }
bool shouldMoveCursorToEnd() const { return moveCursorToEnd_; }
void requestMoveCursorToEnd() { moveCursorToEnd_ = true; }
void clearMoveCursorToEnd() { moveCursorToEnd_ = false; }
// ---- Chat type selection ----
int getSelectedChatType() const { return selectedChatType_; }
void setSelectedChatType(int t) { selectedChatType_ = t; }
int getLastChatType() const { return lastChatType_; }
void setLastChatType(int t) { lastChatType_ = t; }
int getSelectedChannelIdx() const { return selectedChannelIdx_; }
void setSelectedChannelIdx(int i) { selectedChannelIdx_ = i; }
// ---- Link insertion (shift-click) ----
void insertLink(const std::string& link);
// ---- Slash key activation ----
void activateSlashInput();
private:
char buffer_[512] = "";
char whisperBuffer_[256] = "";
bool active_ = false;
bool focusRequested_ = false;
bool moveCursorToEnd_ = false;
// Chat type dropdown state
int selectedChatType_ = 0; // 0=SAY .. 10=CHANNEL
int lastChatType_ = 0;
int selectedChannelIdx_ = 0;
// Sent-message history
std::vector<std::string> sentHistory_;
int historyIdx_ = -1;
static constexpr int kMaxHistory = 50;
};
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,55 @@
#pragma once
#include <imgui.h>
#include <cstdint>
#include <string>
#include <vector>
namespace wowee {
namespace ui {
/**
* Segment types produced by ChatMarkupParser.
*
* Each segment represents a contiguous piece of a chat message
* after WoW markup (|c...|r, |Hitem:...|h[...]|h, URLs) has been decoded.
*/
enum class SegmentType {
Text, // Plain text (render with base message color)
ColoredText, // Text with explicit |cAARRGGBB color
ItemLink, // |Hitem:ID:...|h[Name]|h
SpellLink, // |Hspell:ID:...|h[Name]|h
QuestLink, // |Hquest:ID:LEVEL|h[Name]|h
AchievementLink, // |Hachievement:ID:...|h[Name]|h
Url, // https://... URL
};
/**
* A single parsed segment of a chat message.
*/
struct ChatSegment {
SegmentType type = SegmentType::Text;
std::string text; // display text (or URL)
ImVec4 color = ImVec4(1, 1, 1, 1); // explicit color (for ColoredText / links)
uint32_t id = 0; // itemId / spellId / questId / achievementId
uint32_t extra = 0; // quest level (for QuestLink)
std::string rawLink; // full original markup for shift-click insertion
};
/**
* Parses raw WoW-markup text into a flat list of typed segments.
*
* Extracted from ChatPanel::render() inline lambdas (Phase 2.1 of chat_panel_ref.md).
* Pure logic no ImGui calls, no game-state access. Fully unit-testable.
*/
class ChatMarkupParser {
public:
/** Parse a raw chat message string into ordered segments. */
std::vector<ChatSegment> parse(const std::string& rawMessage) const;
/** Parse |cAARRGGBB color code at given position. */
static ImVec4 parseWowColor(const std::string& text, size_t pos);
};
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,53 @@
#pragma once
#include "ui/chat/chat_markup_parser.hpp"
#include "ui/ui_services.hpp"
#include <vulkan/vulkan.h>
#include <functional>
#include <cstdint>
#include <string>
#include <vector>
namespace wowee {
namespace game { class GameHandler; }
namespace pipeline { class AssetManager; }
namespace ui {
class InventoryScreen;
class SpellbookScreen;
class QuestLogScreen;
/**
* Context needed by the renderer to display links, tooltips, and icons.
*/
struct MarkupRenderContext {
game::GameHandler* gameHandler = nullptr;
InventoryScreen* inventory = nullptr;
SpellbookScreen* spellbook = nullptr;
QuestLogScreen* questLog = nullptr;
pipeline::AssetManager* assetMgr = nullptr;
// Spell icon callback — same as ChatPanel::getSpellIcon
std::function<VkDescriptorSet(uint32_t, pipeline::AssetManager*)> getSpellIcon;
// Chat input buffer for shift-click link insertion
char* chatInputBuffer = nullptr;
size_t chatInputBufSize = 0;
bool* moveCursorToEnd = nullptr;
};
/**
* Renders parsed ChatSegments via ImGui.
*
* Extracted from ChatPanel::render() inline lambdas (Phase 2.2 of chat_panel_ref.md).
* Handles: colored text, item/spell/quest/achievement link tooltips+icons,
* URL click-to-open, shift-click link insertion.
*/
class ChatMarkupRenderer {
public:
/** Render a list of segments with ImGui. baseColor is the message-type color. */
void render(const std::vector<ChatSegment>& segments,
const ImVec4& baseColor,
const MarkupRenderContext& ctx) const;
};
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,40 @@
#pragma once
#include <functional>
// Forward declaration for ImGui (avoid pulling full imgui header)
struct ImGuiContext;
namespace wowee {
namespace ui {
/**
* Chat appearance and auto-join settings.
*
* Extracted from ChatPanel (Phase 1.1 of chat_panel_ref.md).
* Pure data + settings UI; no dependency on GameHandler or network.
*/
struct ChatSettings {
// Appearance
bool showTimestamps = false;
int fontSize = 1; // 0=small, 1=medium, 2=large
// Auto-join channels
bool autoJoinGeneral = true;
bool autoJoinTrade = true;
bool autoJoinLocalDefense = true;
bool autoJoinLFG = true;
bool autoJoinLocal = true;
// Window state
bool windowLocked = true;
/** Reset all chat settings to defaults. */
void restoreDefaults();
/** Render the "Chat" tab inside the Settings window. */
void renderSettingsTab(std::function<void()> saveSettingsFn);
};
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,53 @@
// ChatTabCompleter — cycling tab-completion state machine.
// Extracted from scattered vars in ChatPanel (Phase 5.1).
#pragma once
#include <string>
#include <vector>
namespace wowee {
namespace ui {
/**
* Stateful tab-completion engine.
*
* The caller gathers candidates and calls startCompletion() or cycle().
* The completer stores the prefix, sorted matches, and current index.
*/
class ChatTabCompleter {
public:
/**
* Start a new completion session with the given candidates.
* Resets the index to 0.
*/
void startCompletion(const std::string& prefix, std::vector<std::string> candidates);
/**
* Cycle to the next match. Returns true if there are matches.
* If the prefix changed, the caller should call startCompletion() instead.
*/
bool next();
/** Get the current match, or "" if no matches. */
std::string getCurrentMatch() const;
/** Get the number of matches in the current session. */
size_t matchCount() const { return matches_.size(); }
/** Check if a completion session is active. */
bool isActive() const { return matchIdx_ >= 0; }
/** Get the current prefix. */
const std::string& getPrefix() const { return prefix_; }
/** Reset the completer (e.g. on text change or arrow key). */
void reset();
private:
std::string prefix_;
std::vector<std::string> matches_;
int matchIdx_ = -1;
};
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,54 @@
#pragma once
#include "game/world_packets.hpp"
#include <imgui.h>
#include <cstdint>
#include <deque>
#include <string>
#include <vector>
namespace wowee {
namespace ui {
/**
* Chat tab definitions, unread tracking, type colors, and type names.
*
* Extracted from ChatPanel (Phase 1.3 of chat_panel_ref.md).
* Owns the tab configuration, unread badge counts, and message filtering.
*/
class ChatTabManager {
public:
ChatTabManager();
// ---- Tab access ----
int getTabCount() const { return static_cast<int>(tabs_.size()); }
const std::string& getTabName(int idx) const { return tabs_[idx].name; }
uint64_t getTabTypeMask(int idx) const { return tabs_[idx].typeMask; }
// ---- Unread tracking ----
int getUnreadCount(int idx) const;
void clearUnread(int idx);
/** Scan new messages since last call and increment unread counters for non-active tabs. */
void updateUnread(const std::deque<game::MessageChatData>& history, int activeTab);
// ---- Message filtering ----
bool shouldShowMessage(const game::MessageChatData& msg, int tabIndex) const;
// ---- Chat type helpers (static, no state needed) ----
static const char* getChatTypeName(game::ChatType type);
static ImVec4 getChatTypeColor(game::ChatType type);
private:
struct ChatTab {
std::string name;
uint64_t typeMask;
};
std::vector<ChatTab> tabs_;
std::vector<int> unread_;
size_t seenCount_ = 0;
void initTabs();
};
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,58 @@
#pragma once
#include "game/world_packets.hpp"
#include "game/entity.hpp"
#include <algorithm>
#include <cctype>
#include <memory>
#include <string>
namespace wowee {
// Forward declarations
namespace game {
class GameHandler;
}
namespace ui {
namespace chat_utils {
/** Create a system-type chat message (used 15+ times throughout commands). */
inline game::MessageChatData makeSystemMessage(const std::string& text) {
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = text;
return msg;
}
/** Trim leading/trailing whitespace from a string. */
inline std::string trim(const std::string& s) {
size_t first = s.find_first_not_of(" \t\r\n");
if (first == std::string::npos) return "";
size_t last = s.find_last_not_of(" \t\r\n");
return s.substr(first, last - first + 1);
}
/** Convert string to lowercase (returns copy). */
inline std::string toLower(std::string s) {
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
return s;
}
/**
* Replace $g/$G gender, $n/$N name, $c/$C class, $r/$R race,
* $p/$o/$s/$S pronoun, $b/$B linebreak, and |n linebreak placeholders.
* Extracted from ChatPanel::replaceGenderPlaceholders (Phase 6.6).
*/
std::string replaceGenderPlaceholders(const std::string& text,
game::GameHandler& gameHandler);
/** Get display name for any entity (Player/Unit/GameObject). */
std::string getEntityDisplayName(const std::shared_ptr<game::Entity>& entity);
} // namespace chat_utils
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,63 @@
// GameStateAdapter — wraps GameHandler + Renderer to implement IGameState.
// Phase 4.2 of chat_panel_ref.md.
#pragma once
#include "ui/chat/i_game_state.hpp"
namespace wowee {
namespace game { class GameHandler; }
namespace rendering { class Renderer; }
namespace ui {
/**
* Concrete adapter from GameHandler + Renderer IGameState.
* Flatten complex entity/aura queries into the simple IGameState interface.
*/
class GameStateAdapter : public IGameState {
public:
GameStateAdapter(game::GameHandler& gameHandler, rendering::Renderer* renderer);
// --- GUIDs ---
uint64_t getPlayerGuid() const override;
uint64_t getTargetGuid() const override;
uint64_t getFocusGuid() const override;
uint64_t getPetGuid() const override;
uint64_t getMouseoverGuid() const override;
// --- Player state ---
bool isInCombat() const override;
bool isMounted() const override;
bool isSwimming() const override;
bool isFlying() const override;
bool isCasting() const override;
bool isChanneling() const override;
bool isStealthed() const override;
bool hasPet() const override;
bool isInGroup() const override;
bool isInRaid() const override;
bool isIndoors() const override;
// --- Numeric ---
uint8_t getActiveTalentSpec() const override;
uint32_t getVehicleId() const override;
uint32_t getCurrentCastSpellId() const override;
// --- Spell/aura ---
std::string getSpellName(uint32_t spellId) const override;
bool hasAuraByName(uint64_t targetGuid, const std::string& spellName,
bool wantDebuff) const override;
bool hasFormAura() const override;
// --- Entity queries ---
bool entityExists(uint64_t guid) const override;
bool entityIsDead(uint64_t guid) const override;
bool entityIsHostile(uint64_t guid) const override;
private:
game::GameHandler& gameHandler_;
rendering::Renderer* renderer_;
};
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,272 @@
// GM command metadata — names, security levels, syntax, help text.
// Sourced from AzerothCore GM commands wiki.
// Used for tab-completion and .gmhelp display; the server handles execution.
#pragma once
#include <array>
#include <string_view>
#include <cstdint>
namespace wowee {
namespace ui {
struct GmCommandEntry {
std::string_view name; // e.g. "gm on"
uint8_t security; // 0=player, 1=mod, 2=gm, 3=admin, 4=console
std::string_view syntax; // e.g. ".gm [on/off]"
std::string_view help; // short description
};
// Curated list of the most useful GM commands for a client emulator.
// The full AzerothCore list has 500+ commands — this table covers the
// ones players/GMs actually type regularly, organized by category.
inline constexpr std::array kGmCommands = {
// ── GM mode & info ──────────────────────────────────────
GmCommandEntry{"gm", 1, ".gm [on/off]", "Toggle GM mode or show state"},
GmCommandEntry{"gm on", 1, ".gm on", "Enable GM mode"},
GmCommandEntry{"gm off", 1, ".gm off", "Disable GM mode"},
GmCommandEntry{"gm fly", 2, ".gm fly [on/off]", "Toggle fly mode"},
GmCommandEntry{"gm visible", 2, ".gm visible [on/off]", "Toggle GM visibility"},
GmCommandEntry{"gm chat", 2, ".gm chat [on/off]", "Toggle GM chat badge"},
GmCommandEntry{"gm ingame", 0, ".gm ingame", "List online GMs"},
GmCommandEntry{"gm list", 3, ".gm list", "List all GM accounts"},
// ── Teleportation ───────────────────────────────────────
GmCommandEntry{"tele", 1, ".tele #location", "Teleport to location"},
GmCommandEntry{"tele group", 2, ".tele group #location", "Teleport group to location"},
GmCommandEntry{"tele name", 2, ".tele name $player #location", "Teleport player to location"},
GmCommandEntry{"go xyz", 1, ".go xyz #x #y [#z [#map [#o]]]", "Teleport to coordinates"},
GmCommandEntry{"go creature", 1, ".go creature #guid", "Teleport to creature by GUID"},
GmCommandEntry{"go creature id", 1, ".go creature id #entry", "Teleport to creature by entry"},
GmCommandEntry{"go gameobject", 1, ".go gameobject #guid", "Teleport to gameobject"},
GmCommandEntry{"go graveyard", 1, ".go graveyard #id", "Teleport to graveyard"},
GmCommandEntry{"go taxinode", 1, ".go taxinode #id", "Teleport to taxinode"},
GmCommandEntry{"go trigger", 1, ".go trigger #id", "Teleport to areatrigger"},
GmCommandEntry{"go zonexy", 1, ".go zonexy #x #y [#zone]", "Teleport to zone coordinates"},
GmCommandEntry{"appear", 1, ".appear $player", "Teleport to player"},
GmCommandEntry{"summon", 2, ".summon $player", "Summon player to you"},
GmCommandEntry{"groupsummon", 2, ".groupsummon $player", "Summon player and group"},
GmCommandEntry{"recall", 2, ".recall [$player]", "Return to pre-teleport location"},
GmCommandEntry{"unstuck", 2, ".unstuck $player [inn/graveyard]", "Unstuck player"},
GmCommandEntry{"gps", 1, ".gps", "Show current position info"},
// ── Character & level ───────────────────────────────────
GmCommandEntry{"levelup", 2, ".levelup [$player] [#levels]", "Increase player level"},
GmCommandEntry{"character level", 3, ".character level [$player] [#lvl]", "Set character level"},
GmCommandEntry{"character rename", 2, ".character rename [$name]", "Flag character for rename"},
GmCommandEntry{"character changefaction", 2, ".character changefaction $name", "Flag for faction change"},
GmCommandEntry{"character changerace", 2, ".character changerace $name", "Flag for race change"},
GmCommandEntry{"character customize", 2, ".character customize [$name]", "Flag for customization"},
GmCommandEntry{"character reputation", 2, ".character reputation [$name]", "Show reputation info"},
GmCommandEntry{"character titles", 2, ".character titles [$name]", "Show known titles"},
GmCommandEntry{"pinfo", 2, ".pinfo [$player]", "Show player account info"},
GmCommandEntry{"guid", 2, ".guid", "Show target GUID"},
// ── Items & inventory ───────────────────────────────────
GmCommandEntry{"additem", 2, ".additem #id [#count]", "Add item to inventory"},
GmCommandEntry{"additem set", 2, ".additem set #setid", "Add item set to inventory"},
// ── Spells & auras ──────────────────────────────────────
GmCommandEntry{"learn", 2, ".learn #spell [all]", "Learn a spell"},
GmCommandEntry{"unlearn", 2, ".unlearn #spell [all]", "Unlearn a spell"},
GmCommandEntry{"learn all my class", 2, ".learn all my class", "Learn all class spells"},
GmCommandEntry{"learn all my spells", 2, ".learn all my spells", "Learn all available spells"},
GmCommandEntry{"learn all my talents", 2, ".learn all my talents", "Learn all talents"},
GmCommandEntry{"learn all crafts", 2, ".learn all crafts", "Learn all professions"},
GmCommandEntry{"learn all lang", 2, ".learn all lang", "Learn all languages"},
GmCommandEntry{"learn all recipes", 2, ".learn all recipes [$prof]", "Learn all recipes"},
GmCommandEntry{"cast", 2, ".cast #spell [triggered]", "Cast spell on target"},
GmCommandEntry{"cast self", 2, ".cast self #spell", "Cast spell on self"},
GmCommandEntry{"aura", 2, ".aura #spell", "Add aura to target"},
GmCommandEntry{"unaura", 2, ".unaura #spell", "Remove aura from target"},
GmCommandEntry{"cooldown", 2, ".cooldown [#spell]", "Remove cooldowns"},
GmCommandEntry{"maxskill", 2, ".maxskill", "Max all skills for target"},
GmCommandEntry{"setskill", 2, ".setskill #skill #level [#max]", "Set skill level"},
// ── Modify stats ────────────────────────────────────────
GmCommandEntry{"modify money", 2, ".modify money #amount", "Add/remove money"},
GmCommandEntry{"modify hp", 2, ".modify hp #value", "Set current HP"},
GmCommandEntry{"modify mana", 2, ".modify mana #value", "Set current mana"},
GmCommandEntry{"modify energy", 2, ".modify energy #value", "Set current energy"},
GmCommandEntry{"modify speed all", 2, ".modify speed all #rate", "Set all movement speeds"},
GmCommandEntry{"modify speed fly", 2, ".modify speed fly #rate", "Set fly speed"},
GmCommandEntry{"modify speed walk", 2, ".modify speed walk #rate", "Set walk speed"},
GmCommandEntry{"modify speed swim", 2, ".modify speed swim #rate", "Set swim speed"},
GmCommandEntry{"modify mount", 2, ".modify mount #id #speed", "Display as mounted"},
GmCommandEntry{"modify scale", 2, ".modify scale #rate", "Set model scale"},
GmCommandEntry{"modify honor", 2, ".modify honor #amount", "Add honor points"},
GmCommandEntry{"modify reputation", 2, ".modify reputation #faction #val", "Set faction reputation"},
GmCommandEntry{"modify talentpoints", 2, ".modify talentpoints #amount", "Set talent points"},
GmCommandEntry{"modify gender", 2, ".modify gender male/female", "Change gender"},
// ── Cheats ──────────────────────────────────────────────
GmCommandEntry{"cheat god", 2, ".cheat god [on/off]", "Toggle god mode"},
GmCommandEntry{"cheat casttime", 2, ".cheat casttime [on/off]", "Toggle no cast time"},
GmCommandEntry{"cheat cooldown", 2, ".cheat cooldown [on/off]", "Toggle no cooldowns"},
GmCommandEntry{"cheat power", 2, ".cheat power [on/off]", "Toggle no mana cost"},
GmCommandEntry{"cheat explore", 2, ".cheat explore #flag", "Reveal/hide all maps"},
GmCommandEntry{"cheat taxi", 2, ".cheat taxi on/off", "Toggle all taxi routes"},
GmCommandEntry{"cheat waterwalk", 2, ".cheat waterwalk on/off", "Toggle waterwalk"},
GmCommandEntry{"cheat status", 2, ".cheat status", "Show active cheats"},
// ── NPC ─────────────────────────────────────────────────
GmCommandEntry{"npc add", 3, ".npc add #entry", "Spawn creature"},
GmCommandEntry{"npc delete", 3, ".npc delete [#guid]", "Delete creature"},
GmCommandEntry{"npc info", 1, ".npc info", "Show NPC details"},
GmCommandEntry{"npc guid", 1, ".npc guid", "Show NPC GUID"},
GmCommandEntry{"npc near", 2, ".npc near [#dist]", "List nearby NPCs"},
GmCommandEntry{"npc say", 2, ".npc say $message", "Make NPC say text"},
GmCommandEntry{"npc yell", 2, ".npc yell $message", "Make NPC yell text"},
GmCommandEntry{"npc move", 3, ".npc move [#guid]", "Move NPC to your position"},
GmCommandEntry{"npc set level", 3, ".npc set level #level", "Set NPC level"},
GmCommandEntry{"npc set model", 3, ".npc set model #displayid", "Set NPC model"},
GmCommandEntry{"npc tame", 2, ".npc tame", "Tame selected creature"},
// ── Game objects ────────────────────────────────────────
GmCommandEntry{"gobject add", 3, ".gobject add #entry", "Spawn gameobject"},
GmCommandEntry{"gobject delete", 3, ".gobject delete #guid", "Delete gameobject"},
GmCommandEntry{"gobject info", 1, ".gobject info [#entry]", "Show gameobject info"},
GmCommandEntry{"gobject near", 3, ".gobject near [#dist]", "List nearby objects"},
GmCommandEntry{"gobject move", 3, ".gobject move #guid [#x #y #z]", "Move gameobject"},
GmCommandEntry{"gobject target", 1, ".gobject target [#id]", "Find nearest gameobject"},
GmCommandEntry{"gobject activate", 2, ".gobject activate #guid", "Activate object (door/button)"},
// ── Combat & death ──────────────────────────────────────
GmCommandEntry{"revive", 2, ".revive", "Revive selected/self"},
GmCommandEntry{"die", 2, ".die", "Kill selected/self"},
GmCommandEntry{"damage", 2, ".damage #amount [#school [#spell]]", "Deal damage to target"},
GmCommandEntry{"combatstop", 2, ".combatstop [$player]", "Stop combat for target"},
GmCommandEntry{"freeze", 2, ".freeze [$player]", "Freeze player"},
GmCommandEntry{"unfreeze", 2, ".unfreeze [$player]", "Unfreeze player"},
GmCommandEntry{"dismount", 0, ".dismount", "Dismount if mounted"},
GmCommandEntry{"respawn", 2, ".respawn", "Respawn nearby creatures/GOs"},
GmCommandEntry{"respawn all", 2, ".respawn all", "Respawn all nearby"},
// ── Quests ──────────────────────────────────────────────
GmCommandEntry{"quest add", 2, ".quest add #id", "Add quest to log"},
GmCommandEntry{"quest complete", 2, ".quest complete #id", "Complete quest objectives"},
GmCommandEntry{"quest remove", 2, ".quest remove #id", "Remove quest from log"},
GmCommandEntry{"quest reward", 2, ".quest reward #id", "Grant quest reward"},
GmCommandEntry{"quest status", 2, ".quest status #id [$name]", "Show quest status"},
// ── Honor & arena ───────────────────────────────────────
GmCommandEntry{"honor add", 2, ".honor add #amount", "Add honor points"},
GmCommandEntry{"honor update", 2, ".honor update", "Force honor field update"},
GmCommandEntry{"achievement add", 2, ".achievement add #id", "Add achievement to target"},
// ── Group & guild ───────────────────────────────────────
GmCommandEntry{"group list", 2, ".group list [$player]", "List group members"},
GmCommandEntry{"group revive", 2, ".group revive $player", "Revive group members"},
GmCommandEntry{"group disband", 2, ".group disband [$player]", "Disband player's group"},
GmCommandEntry{"guild create", 2, ".guild create $leader \"$name\"", "Create guild"},
GmCommandEntry{"guild delete", 2, ".guild delete \"$name\"", "Delete guild"},
GmCommandEntry{"guild invite", 2, ".guild invite $player \"$guild\"", "Add player to guild"},
GmCommandEntry{"guild info", 2, ".guild info", "Show guild info"},
// ── Lookup & search ─────────────────────────────────────
GmCommandEntry{"lookup item", 1, ".lookup item $name", "Search item by name"},
GmCommandEntry{"lookup spell", 1, ".lookup spell $name", "Search spell by name"},
GmCommandEntry{"lookup spell id", 1, ".lookup spell id #id", "Look up spell by ID"},
GmCommandEntry{"lookup creature", 1, ".lookup creature $name", "Search creature by name"},
GmCommandEntry{"lookup quest", 1, ".lookup quest $name", "Search quest by name"},
GmCommandEntry{"lookup gobject", 1, ".lookup gobject $name", "Search gameobject by name"},
GmCommandEntry{"lookup area", 1, ".lookup area $name", "Search area by name"},
GmCommandEntry{"lookup taxinode", 1, ".lookup taxinode $name", "Search taxinode by name"},
GmCommandEntry{"lookup teleport", 1, ".lookup teleport $name", "Search teleport by name"},
GmCommandEntry{"lookup faction", 1, ".lookup faction $name", "Search faction by name"},
GmCommandEntry{"lookup title", 1, ".lookup title $name", "Search title by name"},
GmCommandEntry{"lookup event", 1, ".lookup event $name", "Search event by name"},
GmCommandEntry{"lookup map", 1, ".lookup map $name", "Search map by name"},
GmCommandEntry{"lookup skill", 1, ".lookup skill $name", "Search skill by name"},
// ── Titles ──────────────────────────────────────────────
GmCommandEntry{"titles add", 2, ".titles add #id", "Add title to target"},
GmCommandEntry{"titles remove", 2, ".titles remove #id", "Remove title from target"},
GmCommandEntry{"titles current", 2, ".titles current #id", "Set current title"},
// ── Morph & display ─────────────────────────────────────
GmCommandEntry{"morph", 1, ".morph #displayid", "Change your model"},
GmCommandEntry{"morph target", 1, ".morph target #displayid", "Change target model"},
GmCommandEntry{"morph mount", 1, ".morph mount #displayid", "Change mount model"},
GmCommandEntry{"morph reset", 1, ".morph reset", "Reset target model"},
// ── Debug & info ────────────────────────────────────────
GmCommandEntry{"debug anim", 3, ".debug anim", "Debug animation"},
GmCommandEntry{"debug arena", 3, ".debug arena", "Toggle arena debug"},
GmCommandEntry{"debug bg", 3, ".debug bg", "Toggle BG debug"},
GmCommandEntry{"debug los", 3, ".debug los", "Show line of sight info"},
GmCommandEntry{"debug loot", 2, ".debug loot $type $id [#count]", "Simulate loot generation"},
GmCommandEntry{"list auras", 1, ".list auras", "List auras on target"},
GmCommandEntry{"list creature", 1, ".list creature #id [#max]", "List creature spawns"},
GmCommandEntry{"list item", 1, ".list item #id [#max]", "List item locations"},
// ── Server & system ─────────────────────────────────────
GmCommandEntry{"announce", 2, ".announce $message", "Broadcast to all players"},
GmCommandEntry{"gmannounce", 2, ".gmannounce $message", "Broadcast to online GMs"},
GmCommandEntry{"notify", 2, ".notify $message", "On-screen broadcast"},
GmCommandEntry{"server info", 0, ".server info", "Show server version/players"},
GmCommandEntry{"server motd", 0, ".server motd", "Show message of the day"},
GmCommandEntry{"commands", 0, ".commands", "List available commands"},
GmCommandEntry{"help", 0, ".help [$cmd]", "Show command help"},
GmCommandEntry{"save", 0, ".save", "Save your character"},
GmCommandEntry{"saveall", 2, ".saveall", "Save all characters"},
// ── Account & bans ──────────────────────────────────────
GmCommandEntry{"account", 0, ".account", "Show account info"},
GmCommandEntry{"account set gmlevel", 4, ".account set gmlevel $acct #lvl", "Set GM security level"},
GmCommandEntry{"ban account", 2, ".ban account $name $time $reason", "Ban account"},
GmCommandEntry{"ban character", 2, ".ban character $name $time $reason", "Ban character"},
GmCommandEntry{"ban ip", 2, ".ban ip $ip $time $reason", "Ban IP address"},
GmCommandEntry{"unban account", 3, ".unban account $name", "Unban account"},
GmCommandEntry{"unban character", 3, ".unban character $name", "Unban character"},
GmCommandEntry{"unban ip", 3, ".unban ip $ip", "Unban IP address"},
GmCommandEntry{"kick", 2, ".kick [$player] [$reason]", "Kick player from world"},
GmCommandEntry{"mute", 2, ".mute $player $minutes [$reason]", "Mute player chat"},
GmCommandEntry{"unmute", 2, ".unmute [$player]", "Unmute player"},
// ── Misc ────────────────────────────────────────────────
GmCommandEntry{"distance", 3, ".distance", "Distance to selected target"},
GmCommandEntry{"wchange", 3, ".wchange #type #grade", "Change weather"},
GmCommandEntry{"mailbox", 1, ".mailbox", "Open mailbox"},
GmCommandEntry{"played", 0, ".played", "Show time played"},
GmCommandEntry{"gear repair", 2, ".gear repair", "Repair all gear"},
GmCommandEntry{"gear stats", 0, ".gear stats", "Show avg item level"},
GmCommandEntry{"reset talents", 3, ".reset talents [$player]", "Reset talents"},
GmCommandEntry{"reset spells", 3, ".reset spells [$player]", "Reset spells"},
GmCommandEntry{"pet create", 2, ".pet create", "Create pet from target"},
GmCommandEntry{"pet learn", 2, ".pet learn #spell", "Teach spell to pet"},
// ── Waypoints ───────────────────────────────────────────
GmCommandEntry{"wp add", 3, ".wp add", "Add waypoint at your position"},
GmCommandEntry{"wp show", 3, ".wp show on/off", "Toggle waypoint display"},
GmCommandEntry{"wp load", 3, ".wp load #pathid", "Load path for creature"},
GmCommandEntry{"wp unload", 3, ".wp unload", "Unload creature path"},
// ── Instance ────────────────────────────────────────────
GmCommandEntry{"instance listbinds", 1, ".instance listbinds", "Show instance binds"},
GmCommandEntry{"instance unbind", 2, ".instance unbind <map|all>", "Clear instance binds"},
GmCommandEntry{"instance stats", 1, ".instance stats", "Show instance stats"},
// ── Events ──────────────────────────────────────────────
GmCommandEntry{"event activelist", 2, ".event activelist", "Show active events"},
GmCommandEntry{"event start", 2, ".event start #id", "Start event"},
GmCommandEntry{"event stop", 2, ".event stop #id", "Stop event"},
GmCommandEntry{"event info", 2, ".event info #id", "Show event info"},
// ── Reload (common) ─────────────────────────────────────
GmCommandEntry{"reload all", 3, ".reload all", "Reload all tables"},
GmCommandEntry{"reload creature_template", 3, ".reload creature_template #entry", "Reload creature template"},
GmCommandEntry{"reload quest_template", 3, ".reload quest_template", "Reload quest templates"},
GmCommandEntry{"reload config", 3, ".reload config", "Reload server config"},
GmCommandEntry{"reload game_tele", 3, ".reload game_tele", "Reload teleport locations"},
// ── Ticket ──────────────────────────────────────────────
GmCommandEntry{"ticket list", 2, ".ticket list", "List open GM tickets"},
GmCommandEntry{"ticket close", 2, ".ticket close #id", "Close ticket"},
GmCommandEntry{"ticket delete", 3, ".ticket delete #id", "Delete ticket permanently"},
GmCommandEntry{"ticket viewid", 2, ".ticket viewid #id", "View ticket details"},
};
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,57 @@
// IChatCommand — interface for all slash commands.
// Phase 3.1 of chat_panel_ref.md.
#pragma once
#include <string>
#include <vector>
namespace wowee {
// Forward declarations
namespace game { class GameHandler; }
namespace ui { struct UIServices; class ChatPanel; }
namespace ui {
/**
* Context passed to every command's execute() method.
* Provides everything a command needs without coupling to ChatPanel.
*/
struct ChatCommandContext {
game::GameHandler& gameHandler;
UIServices& services;
ChatPanel& panel; // for input buffer access, macro state
std::string args; // everything after "/cmd "
std::string fullCommand; // the original command name (lowercase)
};
/**
* Result returned by a command to tell the dispatcher what to do next.
*/
struct ChatCommandResult {
bool handled = true; // false → command not recognized, fall through
bool clearInput = true; // clear the input buffer after execution
};
/**
* Interface for all chat slash commands.
*
* Adding a new command = create a class implementing this interface,
* register it in ChatCommandRegistry. Zero edits to existing code. (OCP)
*/
class IChatCommand {
public:
virtual ~IChatCommand() = default;
/** Execute the command. */
virtual ChatCommandResult execute(ChatCommandContext& ctx) = 0;
/** Return all aliases for this command (e.g. {"w", "whisper", "tell", "t"}). */
virtual std::vector<std::string> aliases() const = 0;
/** Optional help text shown by /help. */
virtual std::string helpText() const { return ""; }
};
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,63 @@
// IGameState — abstract interface for game state queries used by macro evaluation.
// Allows unit testing with mock state. Phase 4.1 of chat_panel_ref.md.
#pragma once
#include <cstdint>
#include <string>
namespace wowee {
namespace ui {
/**
* Read-only view of game state for macro conditional evaluation.
*
* All entity/aura queries are flattened to simple types so callers
* don't need to depend on game::Entity, game::Unit, etc.
*/
class IGameState {
public:
virtual ~IGameState() = default;
// --- GUIDs ---
virtual uint64_t getPlayerGuid() const = 0;
virtual uint64_t getTargetGuid() const = 0;
virtual uint64_t getFocusGuid() const = 0;
virtual uint64_t getPetGuid() const = 0;
virtual uint64_t getMouseoverGuid() const = 0;
// --- Player state booleans ---
virtual bool isInCombat() const = 0;
virtual bool isMounted() const = 0;
virtual bool isSwimming() const = 0;
virtual bool isFlying() const = 0;
virtual bool isCasting() const = 0;
virtual bool isChanneling() const = 0;
virtual bool isStealthed() const = 0;
virtual bool hasPet() const = 0;
virtual bool isInGroup() const = 0;
virtual bool isInRaid() const = 0;
virtual bool isIndoors() const = 0;
// --- Numeric state ---
virtual uint8_t getActiveTalentSpec() const = 0; // 0-based index
virtual uint32_t getVehicleId() const = 0;
virtual uint32_t getCurrentCastSpellId() const = 0;
// --- Spell/aura queries ---
virtual std::string getSpellName(uint32_t spellId) const = 0;
/** Check if target (or player if guid==playerGuid) has a buff/debuff by name. */
virtual bool hasAuraByName(uint64_t targetGuid, const std::string& spellName,
bool wantDebuff) const = 0;
/** Check if player has a form/stance aura (permanent aura, maxDurationMs == -1). */
virtual bool hasFormAura() const = 0;
// --- Entity queries (flattened, no Entity* exposure) ---
virtual bool entityExists(uint64_t guid) const = 0;
virtual bool entityIsDead(uint64_t guid) const = 0;
virtual bool entityIsHostile(uint64_t guid) const = 0;
};
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,21 @@
// IModifierState — abstract interface for keyboard modifier queries.
// Allows unit testing macro conditionals without real input system. Phase 4.1.
#pragma once
namespace wowee {
namespace ui {
/**
* Read-only view of keyboard modifier state for macro conditional evaluation.
*/
class IModifierState {
public:
virtual ~IModifierState() = default;
virtual bool isShiftHeld() const = 0;
virtual bool isCtrlHeld() const = 0;
virtual bool isAltHeld() const = 0;
};
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,22 @@
// InputModifierAdapter — wraps core::Input to implement IModifierState.
// Phase 4.3 of chat_panel_ref.md.
#pragma once
#include "ui/chat/i_modifier_state.hpp"
namespace wowee {
namespace ui {
/**
* Concrete adapter from core::Input IModifierState.
* Reads real keyboard state from SDL.
*/
class InputModifierAdapter : public IModifierState {
public:
bool isShiftHeld() const override;
bool isCtrlHeld() const override;
bool isAltHeld() const override;
};
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,28 @@
#pragma once
#include <cstdint>
namespace wowee {
namespace game { class GameHandler; }
namespace pipeline { class AssetManager; }
namespace ui {
class InventoryScreen;
/**
* Renders a full WoW-style item tooltip via ImGui.
*
* Extracted from ChatMarkupRenderer::renderItemTooltip (Phase 6.7).
* Handles: item name/quality color, armor/DPS, stats, sockets, set bonuses,
* durability, sell price, required level, comparison tooltip.
*/
class ItemTooltipRenderer {
public:
static void render(uint32_t itemEntry,
game::GameHandler& gameHandler,
InventoryScreen& inventoryScreen,
pipeline::AssetManager* assetMgr);
};
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,57 @@
// MacroEvaluator — WoW macro conditional parser and evaluator.
// Extracted from evaluateMacroConditionals() in chat_panel_commands.cpp.
// Phase 4.4 of chat_panel_ref.md.
#pragma once
#include <cstdint>
#include <string>
namespace wowee {
namespace game { class GameHandler; }
namespace ui {
class IGameState;
class IModifierState;
/**
* Evaluates WoW-style macro conditional expressions.
*
* Syntax: [cond1,cond2] Spell1; [cond3] Spell2; DefaultSpell
*
* The first alternative whose conditions all evaluate true is returned.
* If no conditions match, returns "".
*
* @p targetOverride is set to a specific GUID if [target=X] or [@X]
* was in the matching conditions, or left as UINT64_MAX for "use normal target".
*/
class MacroEvaluator {
public:
MacroEvaluator(IGameState& gameState, IModifierState& modState);
/**
* Evaluate a macro conditional string.
* @param rawArg The conditional text (e.g. "[combat] Spell1; Spell2")
* @param targetOverride Output: set to target GUID if specified, or -1
* @return The matched argument text, or "" if nothing matched
*/
std::string evaluate(const std::string& rawArg, uint64_t& targetOverride) const;
private:
/** Evaluate a single condition token (e.g. "combat", "mod:shift", "@focus"). */
bool evalCondition(const std::string& cond, uint64_t& tgt) const;
/** Resolve effective target GUID (follows @/target= overrides). */
uint64_t resolveEffectiveTarget(uint64_t tgt) const;
IGameState& gameState_;
IModifierState& modState_;
};
// Convenience free function — thin wrapper over MacroEvaluator.
// Used by command modules (combat_commands, system_commands, target_commands).
std::string evaluateMacroConditionals(const std::string& rawArg,
game::GameHandler& gameHandler,
uint64_t& targetOverride);
} // namespace ui
} // namespace wowee

View file

@ -2,6 +2,15 @@
#include "game/game_handler.hpp"
#include "ui/ui_services.hpp"
#include "ui/chat/chat_settings.hpp"
#include "ui/chat/chat_input.hpp"
#include "ui/chat/chat_tab_manager.hpp"
#include "ui/chat/chat_bubble_manager.hpp"
#include "ui/chat/cast_sequence_tracker.hpp"
#include "ui/chat/chat_markup_parser.hpp"
#include "ui/chat/chat_markup_renderer.hpp"
#include "ui/chat/chat_command_registry.hpp"
#include "ui/chat/chat_tab_completer.hpp"
#include <vulkan/vulkan.h>
#include <imgui.h>
#include <string>
@ -69,9 +78,6 @@ public:
/** Execute a macro body (one line per 'click'). */
void executeMacroText(game::GameHandler& gameHandler,
InventoryScreen& inventoryScreen,
SpellbookScreen& spellbookScreen,
QuestLogScreen& questLogScreen,
const std::string& macroText);
// ---- Slash-command side-effects ----
@ -90,40 +96,60 @@ public:
/** Return accumulated slash-command flags and reset them. */
SlashCommands consumeSlashCommands();
// ---- Chat settings (read/written by GameScreen save/load & settings tab) ----
// ---- Chat settings (delegated to ChatSettings) ----
bool chatShowTimestamps = false;
int chatFontSize = 1; // 0=small, 1=medium, 2=large
bool chatAutoJoinGeneral = true;
bool chatAutoJoinTrade = true;
bool chatAutoJoinLocalDefense = true;
bool chatAutoJoinLFG = true;
bool chatAutoJoinLocal = true;
ChatSettings settings;
int activeChatTab = 0;
// Legacy accessors — forward to settings struct for external code
// (GameScreen save/load reads these directly)
bool& chatShowTimestamps = settings.showTimestamps;
int& chatFontSize = settings.fontSize;
bool& chatAutoJoinGeneral = settings.autoJoinGeneral;
bool& chatAutoJoinTrade = settings.autoJoinTrade;
bool& chatAutoJoinLocalDefense = settings.autoJoinLocalDefense;
bool& chatAutoJoinLFG = settings.autoJoinLFG;
bool& chatAutoJoinLocal = settings.autoJoinLocal;
/** Spell icon lookup callback — set by GameScreen each frame before render(). */
std::function<VkDescriptorSet(uint32_t, pipeline::AssetManager*)> getSpellIcon;
/** Render the "Chat" tab inside the Settings window. */
void renderSettingsTab(std::function<void()> saveSettingsFn);
/** Render the "Chat" tab inside the Settings window (delegates to settings). */
void renderSettingsTab(std::function<void()> saveSettingsFn) {
settings.renderSettingsTab(std::move(saveSettingsFn));
}
/** Reset all chat settings to defaults. */
void restoreDefaults();
/** Reset all chat settings to defaults (delegates to settings). */
void restoreDefaults() { settings.restoreDefaults(); }
// UIServices injection (Phase B singleton breaking)
void setServices(const UIServices& services) { services_ = services; }
/** Replace $g/$G and $n/$N gender/name placeholders in quest/chat text. */
std::string replaceGenderPlaceholders(const std::string& text, game::GameHandler& gameHandler);
// ---- Accessors for command system (Phase 3) ----
char* getChatInputBuffer() { return chatInputBuffer_; }
size_t getChatInputBufferSize() const { return sizeof(chatInputBuffer_); }
char* getWhisperTargetBuffer() { return whisperTargetBuffer_; }
size_t getWhisperTargetBufferSize() const { return sizeof(whisperTargetBuffer_); }
int getSelectedChatType() const { return selectedChatType_; }
void setSelectedChatType(int t) { selectedChatType_ = t; }
int getSelectedChannelIdx() const { return selectedChannelIdx_; }
bool& macroStopped() { return macroStopped_; }
CastSequenceTracker& getCastSeqTracker() { return castSeqTracker_; }
SlashCommands& getSlashCmds() { return slashCmds_; }
UIServices& getServices() { return services_; }
ChatCommandRegistry& getCommandRegistry() { return commandRegistry_; }
private:
// Injected UI services (Phase B singleton breaking)
UIServices services_;
// ---- Chat input state ----
// NOTE: These will migrate to ChatInput in Phase 6 (slim ChatPanel).
// ChatInput class is ready at include/ui/chat/chat_input.hpp.
char chatInputBuffer_[512] = "";
char whisperTargetBuffer_[256] = "";
bool chatInputActive_ = false;
int chatInputCooldown_ = 0; // frames to suppress re-activation after send
int selectedChatType_ = 0; // 0=SAY .. 10=CHANNEL
int lastChatType_ = 0;
int selectedChannelIdx_ = 0;
@ -137,55 +163,45 @@ private:
// Macro stop flag
bool macroStopped_ = false;
// Tab-completion state
std::string chatTabPrefix_;
std::vector<std::string> chatTabMatches_;
int chatTabMatchIdx_ = -1;
// /castsequence state (delegated to CastSequenceTracker, Phase 1.5)
CastSequenceTracker castSeqTracker_;
// Command registry (Phase 3 — replaces if/else chain)
ChatCommandRegistry commandRegistry_;
void registerAllCommands();
// Markup parser + renderer (Phase 2)
ChatMarkupParser markupParser_;
ChatMarkupRenderer markupRenderer_;
// Tab-completion (Phase 5 — delegated to ChatTabCompleter)
ChatTabCompleter tabCompleter_;
// Mention notification
size_t chatMentionSeenCount_ = 0;
// ---- Chat tabs ----
struct ChatTab {
std::string name;
uint64_t typeMask;
};
std::vector<ChatTab> chatTabs_;
std::vector<int> chatTabUnread_;
size_t chatTabSeenCount_ = 0;
void initChatTabs();
bool shouldShowMessage(const game::MessageChatData& msg, int tabIndex) const;
// ---- Chat tabs (delegated to ChatTabManager) ----
ChatTabManager tabManager_;
// ---- Chat window visual state ----
bool chatScrolledUp_ = false;
bool chatForceScrollToBottom_ = false;
bool chatWindowLocked_ = true;
// windowLocked is in settings.windowLocked (kept in sync via reference)
bool& chatWindowLocked_ = settings.windowLocked;
ImVec2 chatWindowPos_ = ImVec2(0.0f, 0.0f);
bool chatWindowPosInit_ = false;
// ---- Chat bubbles ----
struct ChatBubble {
uint64_t senderGuid = 0;
std::string message;
float timeRemaining = 0.0f;
float totalDuration = 0.0f;
bool isYell = false;
};
std::vector<ChatBubble> chatBubbles_;
bool chatBubbleCallbackSet_ = false;
// ---- Chat bubbles (delegated to ChatBubbleManager) ----
ChatBubbleManager bubbleManager_;
// ---- Whisper toast state (populated in render, rendered by GameScreen/ToastManager) ----
// Whisper scanning lives here because it's tightly coupled to chat history iteration.
size_t whisperSeenCount_ = 0;
// ---- Helpers ----
void sendChatMessage(game::GameHandler& gameHandler,
InventoryScreen& inventoryScreen,
SpellbookScreen& spellbookScreen,
QuestLogScreen& questLogScreen);
const char* getChatTypeName(game::ChatType type) const;
ImVec4 getChatTypeColor(game::ChatType type) const;
void sendChatMessage(game::GameHandler& gameHandler);
static int inputTextCallback(ImGuiInputTextCallbackData* data);
void detectChannelPrefix(game::GameHandler& gameHandler);
// Cached game handler for input callback (set each frame in render)
game::GameHandler* cachedGameHandler_ = nullptr;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,192 @@
// ChatMarkupRenderer — render parsed ChatSegments via ImGui.
// Moved from ChatPanel::render() inline lambdas (Phase 2.2).
// Item tooltip rendering extracted to ItemTooltipRenderer (Phase 6.7).
#include "ui/chat/chat_markup_renderer.hpp"
#include "ui/chat/item_tooltip_renderer.hpp"
#include "ui/ui_colors.hpp"
#include "ui/inventory_screen.hpp"
#include "ui/spellbook_screen.hpp"
#include "ui/quest_log_screen.hpp"
#include "game/game_handler.hpp"
#include "pipeline/asset_manager.hpp"
#include <imgui.h>
#include <cstring>
namespace wowee { namespace ui {
// ---- 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);
ItemTooltipRenderer::render(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) {
ItemTooltipRenderer::render(itemEntry, *ctx.gameHandler, *ctx.inventory, ctx.assetMgr);
}
}
// Shift-click: insert entire link back into chat input
if (ImGui::IsItemClicked() && ImGui::GetIO().KeyShift &&
ctx.chatInputBuffer && ctx.moveCursorToEnd) {
size_t curLen = strlen(ctx.chatInputBuffer);
if (curLen + seg.rawLink.size() + 1 < ctx.chatInputBufSize) {
strncat(ctx.chatInputBuffer, seg.rawLink.c_str(), ctx.chatInputBufSize - curLen - 1);
*ctx.moveCursorToEnd = true;
}
}
if (needSameLine) ImGui::SameLine(0, 0);
break;
}
case SegmentType::SpellLink: {
// Small icon (use spell icon cache if available)
VkDescriptorSet spellIcon = VK_NULL_HANDLE;
if (seg.id > 0 && ctx.getSpellIcon && ctx.assetMgr) {
spellIcon = ctx.getSpellIcon(seg.id, ctx.assetMgr);
}
if (spellIcon) {
ImGui::Image((ImTextureID)(uintptr_t)spellIcon, ImVec2(12, 12));
if (ImGui::IsItemHovered() && ctx.spellbook && ctx.gameHandler && ctx.assetMgr) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ctx.spellbook->renderSpellInfoTooltip(seg.id, *ctx.gameHandler, ctx.assetMgr);
}
ImGui::SameLine(0, 2);
}
std::string display = "[" + seg.text + "]";
ImGui::PushStyleColor(ImGuiCol_Text, seg.color);
ImGui::TextWrapped("%s", display.c_str());
ImGui::PopStyleColor();
if (ImGui::IsItemHovered() && ctx.spellbook && ctx.gameHandler && ctx.assetMgr) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ctx.spellbook->renderSpellInfoTooltip(seg.id, *ctx.gameHandler, ctx.assetMgr);
}
// Shift-click: insert link
if (ImGui::IsItemClicked() && ImGui::GetIO().KeyShift &&
ctx.chatInputBuffer && ctx.moveCursorToEnd) {
size_t curLen = strlen(ctx.chatInputBuffer);
if (curLen + seg.rawLink.size() + 1 < ctx.chatInputBufSize) {
strncat(ctx.chatInputBuffer, seg.rawLink.c_str(), ctx.chatInputBufSize - curLen - 1);
*ctx.moveCursorToEnd = true;
}
}
if (needSameLine) ImGui::SameLine(0, 0);
break;
}
case SegmentType::QuestLink: {
std::string display = "[" + seg.text + "]";
ImGui::PushStyleColor(ImGuiCol_Text, colors::kWarmGold);
ImGui::TextWrapped("%s", display.c_str());
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::BeginTooltip();
ImGui::TextColored(colors::kWarmGold, "%s", seg.text.c_str());
if (seg.extra > 0) ImGui::TextDisabled("Level %u Quest", seg.extra);
ImGui::TextDisabled("Click quest log to view details");
ImGui::EndTooltip();
}
if (ImGui::IsItemClicked() && seg.id > 0 && ctx.questLog) {
ctx.questLog->openAndSelectQuest(seg.id);
}
// Shift-click: insert link
if (ImGui::IsItemClicked() && ImGui::GetIO().KeyShift &&
ctx.chatInputBuffer && ctx.moveCursorToEnd) {
size_t curLen = strlen(ctx.chatInputBuffer);
if (curLen + seg.rawLink.size() + 1 < ctx.chatInputBufSize) {
strncat(ctx.chatInputBuffer, seg.rawLink.c_str(), ctx.chatInputBufSize - curLen - 1);
*ctx.moveCursorToEnd = true;
}
}
if (needSameLine) ImGui::SameLine(0, 0);
break;
}
case SegmentType::AchievementLink: {
std::string display = "[" + seg.text + "]";
ImGui::PushStyleColor(ImGuiCol_Text, colors::kBrightGold);
ImGui::TextWrapped("%s", display.c_str());
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Achievement: %s", seg.text.c_str());
}
// Shift-click: insert link
if (ImGui::IsItemClicked() && ImGui::GetIO().KeyShift &&
ctx.chatInputBuffer && ctx.moveCursorToEnd) {
size_t curLen = strlen(ctx.chatInputBuffer);
if (curLen + seg.rawLink.size() + 1 < ctx.chatInputBufSize) {
strncat(ctx.chatInputBuffer, seg.rawLink.c_str(), ctx.chatInputBufSize - curLen - 1);
*ctx.moveCursorToEnd = true;
}
}
if (needSameLine) ImGui::SameLine(0, 0);
break;
}
case SegmentType::Url: {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.7f, 1.0f, 1.0f));
ImGui::TextWrapped("%s", seg.text.c_str());
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Open: %s", seg.text.c_str());
}
if (ImGui::IsItemClicked()) {
std::string cmd = "xdg-open '" + seg.text + "' &";
[[maybe_unused]] int result = system(cmd.c_str());
}
ImGui::PopStyleColor();
if (needSameLine) ImGui::SameLine(0, 0);
break;
}
} // switch
}
}
} // namespace ui
} // namespace wowee

View file

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

View file

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

View file

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

157
src/ui/chat/chat_utils.cpp Normal file
View file

@ -0,0 +1,157 @@
// chat_utils.cpp — Shared chat utility functions.
// Extracted from chat_panel_utils.cpp (Phase 6.6 of chat_panel_ref.md).
#include "ui/chat/chat_utils.hpp"
#include "game/game_handler.hpp"
#include "game/character.hpp"
#include <vector>
namespace wowee { namespace ui { namespace chat_utils {
std::string replaceGenderPlaceholders(const std::string& text,
game::GameHandler& gameHandler) {
// Get player gender, pronouns, and name
game::Gender gender = game::Gender::NONBINARY;
std::string playerName = "Adventurer";
const auto* character = gameHandler.getActiveCharacter();
if (character) {
gender = character->gender;
if (!character->name.empty()) {
playerName = character->name;
}
}
game::Pronouns pronouns = game::Pronouns::forGender(gender);
std::string result = text;
// Helper to trim whitespace
auto trimStr = [](std::string& s) {
const char* ws = " \t\n\r";
size_t start = s.find_first_not_of(ws);
if (start == std::string::npos) { s.clear(); return; }
size_t end = s.find_last_not_of(ws);
s = s.substr(start, end - start + 1);
};
// Replace $g/$G placeholders first.
size_t pos = 0;
while ((pos = result.find('$', pos)) != std::string::npos) {
if (pos + 1 >= result.length()) break;
char marker = result[pos + 1];
if (marker != 'g' && marker != 'G') { pos++; continue; }
size_t endPos = result.find(';', pos);
if (endPos == std::string::npos) { pos += 2; continue; }
std::string placeholder = result.substr(pos + 2, endPos - pos - 2);
// Split by colons
std::vector<std::string> parts;
size_t start = 0;
size_t colonPos;
while ((colonPos = placeholder.find(':', start)) != std::string::npos) {
std::string part = placeholder.substr(start, colonPos - start);
trimStr(part);
parts.push_back(part);
start = colonPos + 1;
}
// Add the last part
std::string lastPart = placeholder.substr(start);
trimStr(lastPart);
parts.push_back(lastPart);
// Select appropriate text based on gender
std::string replacement;
if (parts.size() >= 3) {
switch (gender) {
case game::Gender::MALE: replacement = parts[0]; break;
case game::Gender::FEMALE: replacement = parts[1]; break;
case game::Gender::NONBINARY: replacement = parts[2]; break;
}
} else if (parts.size() >= 2) {
switch (gender) {
case game::Gender::MALE: replacement = parts[0]; break;
case game::Gender::FEMALE: replacement = parts[1]; break;
case game::Gender::NONBINARY:
replacement = parts[0].length() <= parts[1].length() ? parts[0] : parts[1];
break;
}
} else {
pos = endPos + 1;
continue;
}
result.replace(pos, endPos - pos + 1, replacement);
pos += replacement.length();
}
// Resolve class and race names for $C and $R placeholders
std::string className = "Adventurer";
std::string raceName = "Unknown";
if (character) {
className = game::getClassName(character->characterClass);
raceName = game::getRaceName(character->race);
}
// Replace simple placeholders.
// $n/$N = player name, $c/$C = class name, $r/$R = race name
// $p = subject pronoun (he/she/they)
// $o = object pronoun (him/her/them)
// $s = possessive adjective (his/her/their)
// $S = possessive pronoun (his/hers/theirs)
// $b/$B = line break
pos = 0;
while ((pos = result.find('$', pos)) != std::string::npos) {
if (pos + 1 >= result.length()) break;
char code = result[pos + 1];
std::string replacement;
switch (code) {
case 'n': case 'N': replacement = playerName; break;
case 'c': case 'C': replacement = className; break;
case 'r': case 'R': replacement = raceName; break;
case 'p': replacement = pronouns.subject; break;
case 'o': replacement = pronouns.object; break;
case 's': replacement = pronouns.possessive; break;
case 'S': replacement = pronouns.possessiveP; break;
case 'b': case 'B': replacement = "\n"; break;
case 'g': case 'G': pos++; continue;
default: pos++; continue;
}
result.replace(pos, 2, replacement);
pos += replacement.length();
}
// WoW markup linebreak token.
pos = 0;
while ((pos = result.find("|n", pos)) != std::string::npos) {
result.replace(pos, 2, "\n");
pos += 1;
}
pos = 0;
while ((pos = result.find("|N", pos)) != std::string::npos) {
result.replace(pos, 2, "\n");
pos += 1;
}
return result;
}
std::string getEntityDisplayName(const std::shared_ptr<game::Entity>& entity) {
if (entity->getType() == game::ObjectType::PLAYER) {
auto player = std::static_pointer_cast<game::Player>(entity);
if (!player->getName().empty()) return player->getName();
} else if (entity->getType() == game::ObjectType::UNIT) {
auto unit = std::static_pointer_cast<game::Unit>(entity);
if (!unit->getName().empty()) return unit->getName();
} else if (entity->getType() == game::ObjectType::GAMEOBJECT) {
auto go = std::static_pointer_cast<game::GameObject>(entity);
if (!go->getName().empty()) return go->getName();
}
return "Unknown";
}
} // namespace chat_utils
} // namespace ui
} // namespace wowee

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,521 @@
// ItemTooltipRenderer — renders full WoW-style item tooltips via ImGui.
// Extracted from ChatMarkupRenderer::renderItemTooltip (Phase 6.7).
#include "ui/chat/item_tooltip_renderer.hpp"
#include "ui/ui_colors.hpp"
#include "ui/inventory_screen.hpp"
#include "game/game_handler.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_loader.hpp"
#include "pipeline/dbc_layout.hpp"
#include <imgui.h>
#include <cstring>
#include <unordered_map>
#include <cstdio>
namespace wowee { namespace ui {
void ItemTooltipRenderer::render(
uint32_t itemEntry,
game::GameHandler& gameHandler,
InventoryScreen& inventoryScreen,
pipeline::AssetManager* assetMgr)
{
const auto* info = gameHandler.getItemInfo(itemEntry);
if (!info || !info->valid) return;
auto findComparableEquipped = [&](uint8_t inventoryType) -> const game::ItemSlot* {
using ES = game::EquipSlot;
const auto& inv = gameHandler.getInventory();
auto slotPtr = [&](ES slot) -> const game::ItemSlot* {
const auto& s = inv.getEquipSlot(slot);
return s.empty() ? nullptr : &s;
};
switch (inventoryType) {
case 1: return slotPtr(ES::HEAD);
case 2: return slotPtr(ES::NECK);
case 3: return slotPtr(ES::SHOULDERS);
case 4: return slotPtr(ES::SHIRT);
case 5:
case 20: return slotPtr(ES::CHEST);
case 6: return slotPtr(ES::WAIST);
case 7: return slotPtr(ES::LEGS);
case 8: return slotPtr(ES::FEET);
case 9: return slotPtr(ES::WRISTS);
case 10: return slotPtr(ES::HANDS);
case 11: {
if (auto* s = slotPtr(ES::RING1)) return s;
return slotPtr(ES::RING2);
}
case 12: {
if (auto* s = slotPtr(ES::TRINKET1)) return s;
return slotPtr(ES::TRINKET2);
}
case 13:
if (auto* s = slotPtr(ES::MAIN_HAND)) return s;
return slotPtr(ES::OFF_HAND);
case 14:
case 22:
case 23: return slotPtr(ES::OFF_HAND);
case 15:
case 25:
case 26: return slotPtr(ES::RANGED);
case 16: return slotPtr(ES::BACK);
case 17:
case 21: return slotPtr(ES::MAIN_HAND);
case 18:
for (int i = 0; i < game::Inventory::NUM_BAG_SLOTS; ++i) {
auto slot = static_cast<ES>(static_cast<int>(ES::BAG1) + i);
if (auto* s = slotPtr(slot)) return s;
}
return nullptr;
case 19: return slotPtr(ES::TABARD);
default: return nullptr;
}
};
auto isWeaponInventoryType = [](uint32_t invType) {
switch (invType) {
case 13: case 15: case 17: case 21: case 25: case 26: return true;
default: return false;
}
};
auto appendBonus = [](std::string& out, int32_t val, const char* shortName) {
if (val <= 0) return;
if (!out.empty()) out += " ";
out += "+" + std::to_string(val) + " ";
out += shortName;
};
ImGui::BeginTooltip();
// Quality color for name
auto qColor = ui::getQualityColor(static_cast<game::ItemQuality>(info->quality));
ImGui::TextColored(qColor, "%s", info->name.c_str());
// Heroic indicator (green, matches WoW tooltip style)
constexpr uint32_t kFlagHeroic = 0x8;
constexpr uint32_t kFlagUniqueEquipped = 0x1000000;
if (info->itemFlags & kFlagHeroic)
ImGui::TextColored(ImVec4(0.0f, 0.8f, 0.0f, 1.0f), "Heroic");
// Bind type (appears right under name in WoW)
switch (info->bindType) {
case 1: ImGui::TextDisabled("Binds when picked up"); break;
case 2: ImGui::TextDisabled("Binds when equipped"); break;
case 3: ImGui::TextDisabled("Binds when used"); break;
case 4: ImGui::TextDisabled("Quest Item"); break;
}
// Unique / Unique-Equipped
if (info->maxCount == 1)
ImGui::TextColored(ui::colors::kTooltipGold, "Unique");
else if (info->itemFlags & kFlagUniqueEquipped)
ImGui::TextColored(ui::colors::kTooltipGold, "Unique-Equipped");
// Slot type
if (info->inventoryType > 0) {
const char* slotName = ui::getInventorySlotName(info->inventoryType);
if (slotName[0]) {
if (!info->subclassName.empty())
ImGui::TextColored(ui::colors::kLightGray, "%s %s", slotName, info->subclassName.c_str());
else
ImGui::TextColored(ui::colors::kLightGray, "%s", slotName);
}
}
const bool isWeapon = isWeaponInventoryType(info->inventoryType);
// Item level (after slot/subclass)
if (info->itemLevel > 0)
ImGui::TextDisabled("Item Level %u", info->itemLevel);
if (isWeapon && info->damageMax > 0.0f && info->delayMs > 0) {
float speed = static_cast<float>(info->delayMs) / 1000.0f;
float dps = ((info->damageMin + info->damageMax) * 0.5f) / speed;
char dmgBuf[64], spdBuf[32];
std::snprintf(dmgBuf, sizeof(dmgBuf), "%d - %d Damage",
static_cast<int>(info->damageMin), static_cast<int>(info->damageMax));
std::snprintf(spdBuf, sizeof(spdBuf), "Speed %.2f", speed);
float spdW = ImGui::CalcTextSize(spdBuf).x;
ImGui::Text("%s", dmgBuf);
ImGui::SameLine(ImGui::GetWindowWidth() - spdW - 16.0f);
ImGui::Text("%s", spdBuf);
ImGui::TextDisabled("(%.1f damage per second)", dps);
}
ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f);
std::string bonusLine;
appendBonus(bonusLine, info->strength, "Str");
appendBonus(bonusLine, info->agility, "Agi");
appendBonus(bonusLine, info->stamina, "Sta");
appendBonus(bonusLine, info->intellect, "Int");
appendBonus(bonusLine, info->spirit, "Spi");
if (!bonusLine.empty()) {
ImGui::TextColored(green, "%s", bonusLine.c_str());
}
if (info->armor > 0) {
ImGui::Text("%d Armor", info->armor);
}
// Elemental resistances
{
const int32_t resVals[6] = {
info->holyRes, info->fireRes, info->natureRes,
info->frostRes, info->shadowRes, info->arcaneRes
};
static constexpr const char* resLabels[6] = {
"Holy Resistance", "Fire Resistance", "Nature Resistance",
"Frost Resistance", "Shadow Resistance", "Arcane Resistance"
};
for (int ri = 0; ri < 6; ++ri)
if (resVals[ri] > 0) ImGui::Text("+%d %s", resVals[ri], resLabels[ri]);
}
// Extra stats (hit/crit/haste/sp/ap/expertise/resilience/etc.)
if (!info->extraStats.empty()) {
auto statName = [](uint32_t t) -> const char* {
switch (t) {
case 12: return "Defense Rating";
case 13: return "Dodge Rating";
case 14: return "Parry Rating";
case 15: return "Block Rating";
case 16: case 17: case 18: case 31: return "Hit Rating";
case 19: case 20: case 21: case 32: return "Critical Strike Rating";
case 28: case 29: case 30: case 35: return "Haste Rating";
case 34: return "Resilience Rating";
case 36: return "Expertise Rating";
case 37: return "Attack Power";
case 38: return "Ranged Attack Power";
case 45: return "Spell Power";
case 46: return "Healing Power";
case 47: return "Spell Damage";
case 49: return "Mana per 5 sec.";
case 43: return "Spell Penetration";
case 44: return "Block Value";
default: return nullptr;
}
};
for (const auto& es : info->extraStats) {
const char* nm = statName(es.statType);
if (nm && es.statValue > 0)
ImGui::TextColored(green, "+%d %s", es.statValue, nm);
}
}
// Gem sockets
{
const auto& kSocketTypes = ui::kSocketTypes;
bool hasSocket = false;
for (int s = 0; s < 3; ++s) {
if (info->socketColor[s] == 0) continue;
if (!hasSocket) { ImGui::Spacing(); hasSocket = true; }
for (const auto& st : kSocketTypes) {
if (info->socketColor[s] & st.mask) {
ImGui::TextColored(st.col, "%s", st.label);
break;
}
}
}
if (hasSocket && info->socketBonus != 0) {
static std::unordered_map<uint32_t, std::string> s_enchantNames;
static bool s_enchantNamesLoaded = false;
if (!s_enchantNamesLoaded && assetMgr) {
s_enchantNamesLoaded = true;
auto dbc = assetMgr->loadDBC("SpellItemEnchantment.dbc");
if (dbc && dbc->isLoaded()) {
const auto* lay = pipeline::getActiveDBCLayout()
? pipeline::getActiveDBCLayout()->getLayout("SpellItemEnchantment") : nullptr;
uint32_t nameField = lay ? lay->field("Name") : 8u;
if (nameField == 0xFFFFFFFF) nameField = 8;
uint32_t fc = dbc->getFieldCount();
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
uint32_t eid = dbc->getUInt32(r, 0);
if (eid == 0 || nameField >= fc) continue;
std::string ename = dbc->getString(r, nameField);
if (!ename.empty()) s_enchantNames[eid] = std::move(ename);
}
}
}
auto enchIt = s_enchantNames.find(info->socketBonus);
if (enchIt != s_enchantNames.end())
ImGui::TextColored(colors::kSocketGreen, "Socket Bonus: %s", enchIt->second.c_str());
else
ImGui::TextColored(colors::kSocketGreen, "Socket Bonus: (id %u)", info->socketBonus);
}
}
// Item set membership
if (info->itemSetId != 0) {
struct SetEntry {
std::string name;
std::array<uint32_t, 10> itemIds{};
std::array<uint32_t, 10> spellIds{};
std::array<uint32_t, 10> thresholds{};
};
static std::unordered_map<uint32_t, SetEntry> s_setData;
static bool s_setDataLoaded = false;
if (!s_setDataLoaded && assetMgr) {
s_setDataLoaded = true;
auto dbc = assetMgr->loadDBC("ItemSet.dbc");
if (dbc && dbc->isLoaded()) {
const auto* layout = pipeline::getActiveDBCLayout()
? pipeline::getActiveDBCLayout()->getLayout("ItemSet") : nullptr;
auto lf = [&](const char* k, uint32_t def) -> uint32_t {
return layout ? (*layout)[k] : def;
};
uint32_t idF = lf("ID", 0), nameF = lf("Name", 1);
const auto& itemKeys = ui::kItemSetItemKeys;
const auto& spellKeys = ui::kItemSetSpellKeys;
const auto& thrKeys = ui::kItemSetThresholdKeys;
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
uint32_t id = dbc->getUInt32(r, idF);
if (!id) continue;
SetEntry e;
e.name = dbc->getString(r, nameF);
for (int i = 0; i < 10; ++i) {
e.itemIds[i] = dbc->getUInt32(r, layout ? (*layout)[itemKeys[i]] : uint32_t(18 + i));
e.spellIds[i] = dbc->getUInt32(r, layout ? (*layout)[spellKeys[i]] : uint32_t(28 + i));
e.thresholds[i] = dbc->getUInt32(r, layout ? (*layout)[thrKeys[i]] : uint32_t(38 + i));
}
s_setData[id] = std::move(e);
}
}
}
ImGui::Spacing();
const auto& inv = gameHandler.getInventory();
auto setIt = s_setData.find(info->itemSetId);
if (setIt != s_setData.end()) {
const SetEntry& se = setIt->second;
int equipped = 0, total = 0;
for (int i = 0; i < 10; ++i) {
if (se.itemIds[i] == 0) continue;
++total;
for (int sl = 0; sl < game::Inventory::NUM_EQUIP_SLOTS; sl++) {
const auto& eq = inv.getEquipSlot(static_cast<game::EquipSlot>(sl));
if (!eq.empty() && eq.item.itemId == se.itemIds[i]) { ++equipped; break; }
}
}
if (total > 0)
ImGui::TextColored(ui::colors::kTooltipGold,
"%s (%d/%d)", se.name.empty() ? "Set" : se.name.c_str(), equipped, total);
else if (!se.name.empty())
ImGui::TextColored(ui::colors::kTooltipGold, "%s", se.name.c_str());
for (int i = 0; i < 10; ++i) {
if (se.spellIds[i] == 0 || se.thresholds[i] == 0) continue;
const std::string& bname = gameHandler.getSpellName(se.spellIds[i]);
bool active = (equipped >= static_cast<int>(se.thresholds[i]));
ImVec4 col = active ? colors::kActiveGreen : colors::kInactiveGray;
if (!bname.empty())
ImGui::TextColored(col, "(%u) %s", se.thresholds[i], bname.c_str());
else
ImGui::TextColored(col, "(%u) Set Bonus", se.thresholds[i]);
}
} else {
ImGui::TextColored(ui::colors::kTooltipGold, "Set (id %u)", info->itemSetId);
}
}
// Item spell effects (Use / Equip / Chance on Hit / Teaches)
for (const auto& sp : info->spells) {
if (sp.spellId == 0) continue;
const char* triggerLabel = nullptr;
switch (sp.spellTrigger) {
case 0: triggerLabel = "Use"; break;
case 1: triggerLabel = "Equip"; break;
case 2: triggerLabel = "Chance on Hit"; break;
case 5: triggerLabel = "Teaches"; break;
}
if (!triggerLabel) continue;
const std::string& spDesc = gameHandler.getSpellDescription(sp.spellId);
const std::string& spText = !spDesc.empty() ? spDesc
: gameHandler.getSpellName(sp.spellId);
if (!spText.empty()) {
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 300.0f);
ImGui::TextColored(colors::kCyan,
"%s: %s", triggerLabel, spText.c_str());
ImGui::PopTextWrapPos();
}
}
// Required level
if (info->requiredLevel > 1)
ImGui::TextDisabled("Requires Level %u", info->requiredLevel);
// Required skill
if (info->requiredSkill != 0 && info->requiredSkillRank > 0) {
static std::unordered_map<uint32_t, std::string> s_skillNames;
static bool s_skillNamesLoaded = false;
if (!s_skillNamesLoaded && assetMgr) {
s_skillNamesLoaded = true;
auto dbc = assetMgr->loadDBC("SkillLine.dbc");
if (dbc && dbc->isLoaded()) {
const auto* layout = pipeline::getActiveDBCLayout()
? pipeline::getActiveDBCLayout()->getLayout("SkillLine") : nullptr;
uint32_t idF = layout ? (*layout)["ID"] : 0u;
uint32_t nameF = layout ? (*layout)["Name"] : 2u;
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
uint32_t sid = dbc->getUInt32(r, idF);
if (!sid) continue;
std::string sname = dbc->getString(r, nameF);
if (!sname.empty()) s_skillNames[sid] = std::move(sname);
}
}
}
uint32_t playerSkillVal = 0;
const auto& skills = gameHandler.getPlayerSkills();
auto skPit = skills.find(info->requiredSkill);
if (skPit != skills.end()) playerSkillVal = skPit->second.effectiveValue();
bool meetsSkill = (playerSkillVal == 0 || playerSkillVal >= info->requiredSkillRank);
ImVec4 skColor = meetsSkill ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : colors::kPaleRed;
auto skIt = s_skillNames.find(info->requiredSkill);
if (skIt != s_skillNames.end())
ImGui::TextColored(skColor, "Requires %s (%u)", skIt->second.c_str(), info->requiredSkillRank);
else
ImGui::TextColored(skColor, "Requires Skill %u (%u)", info->requiredSkill, info->requiredSkillRank);
}
// Required reputation
if (info->requiredReputationFaction != 0 && info->requiredReputationRank > 0) {
static std::unordered_map<uint32_t, std::string> s_factionNames;
static bool s_factionNamesLoaded = false;
if (!s_factionNamesLoaded && assetMgr) {
s_factionNamesLoaded = true;
auto dbc = assetMgr->loadDBC("Faction.dbc");
if (dbc && dbc->isLoaded()) {
const auto* layout = pipeline::getActiveDBCLayout()
? pipeline::getActiveDBCLayout()->getLayout("Faction") : nullptr;
uint32_t idF = layout ? (*layout)["ID"] : 0u;
uint32_t nameF = layout ? (*layout)["Name"] : 20u;
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
uint32_t fid = dbc->getUInt32(r, idF);
if (!fid) continue;
std::string fname = dbc->getString(r, nameF);
if (!fname.empty()) s_factionNames[fid] = std::move(fname);
}
}
}
static constexpr const char* kRepRankNames[] = {
"Hated", "Hostile", "Unfriendly", "Neutral",
"Friendly", "Honored", "Revered", "Exalted"
};
const char* rankName = (info->requiredReputationRank < 8)
? kRepRankNames[info->requiredReputationRank] : "Unknown";
auto fIt = s_factionNames.find(info->requiredReputationFaction);
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.75f), "Requires %s with %s",
rankName,
fIt != s_factionNames.end() ? fIt->second.c_str() : "Unknown Faction");
}
// Class restriction
if (info->allowableClass != 0) {
const auto& kClasses = ui::kClassMasks;
int matchCount = 0;
for (const auto& kc : kClasses)
if (info->allowableClass & kc.mask) ++matchCount;
if (matchCount > 0 && matchCount < 10) {
char classBuf[128] = "Classes: ";
bool first = true;
for (const auto& kc : kClasses) {
if (!(info->allowableClass & kc.mask)) continue;
if (!first) strncat(classBuf, ", ", sizeof(classBuf) - strlen(classBuf) - 1);
strncat(classBuf, kc.name, sizeof(classBuf) - strlen(classBuf) - 1);
first = false;
}
uint8_t pc = gameHandler.getPlayerClass();
uint32_t pmask = (pc > 0 && pc <= 10) ? (1u << (pc - 1)) : 0u;
bool playerAllowed = (pmask == 0 || (info->allowableClass & pmask));
ImVec4 clColor = playerAllowed ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : colors::kPaleRed;
ImGui::TextColored(clColor, "%s", classBuf);
}
}
// Race restriction
if (info->allowableRace != 0) {
const auto& kRaces = ui::kRaceMasks;
constexpr uint32_t kAllPlayable = 1|2|4|8|16|32|64|128|512|1024;
if ((info->allowableRace & kAllPlayable) != kAllPlayable) {
int matchCount = 0;
for (const auto& kr : kRaces)
if (info->allowableRace & kr.mask) ++matchCount;
if (matchCount > 0) {
char raceBuf[160] = "Races: ";
bool first = true;
for (const auto& kr : kRaces) {
if (!(info->allowableRace & kr.mask)) continue;
if (!first) strncat(raceBuf, ", ", sizeof(raceBuf) - strlen(raceBuf) - 1);
strncat(raceBuf, kr.name, sizeof(raceBuf) - strlen(raceBuf) - 1);
first = false;
}
uint8_t pr = gameHandler.getPlayerRace();
uint32_t pmask = (pr > 0 && pr <= 11) ? (1u << (pr - 1)) : 0u;
bool playerAllowed = (pmask == 0 || (info->allowableRace & pmask));
ImVec4 rColor = playerAllowed ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : colors::kPaleRed;
ImGui::TextColored(rColor, "%s", raceBuf);
}
}
}
// Flavor text
if (!info->description.empty()) {
ImGui::Spacing();
ImGui::PushTextWrapPos(300.0f);
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 0.85f), "\"%s\"", info->description.c_str());
ImGui::PopTextWrapPos();
}
if (info->sellPrice > 0) {
ImGui::TextDisabled("Sell:"); ImGui::SameLine(0, 4);
renderCoinsFromCopper(info->sellPrice);
}
if (ImGui::GetIO().KeyShift && info->inventoryType > 0) {
if (const auto* eq = findComparableEquipped(static_cast<uint8_t>(info->inventoryType))) {
ImGui::Separator();
ImGui::TextDisabled("Equipped:");
VkDescriptorSet eqIcon = inventoryScreen.getItemIcon(eq->item.displayInfoId);
if (eqIcon) {
ImGui::Image((ImTextureID)(uintptr_t)eqIcon, ImVec2(18.0f, 18.0f));
ImGui::SameLine();
}
ImGui::TextColored(InventoryScreen::getQualityColor(eq->item.quality), "%s", eq->item.name.c_str());
if (isWeaponInventoryType(eq->item.inventoryType) &&
eq->item.damageMax > 0.0f && eq->item.delayMs > 0) {
float speed = static_cast<float>(eq->item.delayMs) / 1000.0f;
float dps = ((eq->item.damageMin + eq->item.damageMax) * 0.5f) / speed;
char eqDmg[64], eqSpd[32];
std::snprintf(eqDmg, sizeof(eqDmg), "%d - %d Damage",
static_cast<int>(eq->item.damageMin), static_cast<int>(eq->item.damageMax));
std::snprintf(eqSpd, sizeof(eqSpd), "Speed %.2f", speed);
float eqSpdW = ImGui::CalcTextSize(eqSpd).x;
ImGui::Text("%s", eqDmg);
ImGui::SameLine(ImGui::GetWindowWidth() - eqSpdW - 16.0f);
ImGui::Text("%s", eqSpd);
ImGui::TextDisabled("(%.1f damage per second)", dps);
}
if (eq->item.armor > 0) {
ImGui::Text("%d Armor", eq->item.armor);
}
std::string eqBonusLine;
appendBonus(eqBonusLine, eq->item.strength, "Str");
appendBonus(eqBonusLine, eq->item.agility, "Agi");
appendBonus(eqBonusLine, eq->item.stamina, "Sta");
appendBonus(eqBonusLine, eq->item.intellect, "Int");
appendBonus(eqBonusLine, eq->item.spirit, "Spi");
if (!eqBonusLine.empty()) {
ImGui::TextColored(green, "%s", eqBonusLine.c_str());
}
// Extra stats for the equipped item
for (const auto& es : eq->item.extraStats) {
const char* nm = nullptr;
switch (es.statType) {
case 12: nm = "Defense Rating"; break;
case 13: nm = "Dodge Rating"; break;
case 14: nm = "Parry Rating"; break;
case 16: case 17: case 18: case 31: nm = "Hit Rating"; break;
case 19: case 20: case 21: case 32: nm = "Critical Strike Rating"; break;
case 28: case 29: case 30: case 35: nm = "Haste Rating"; break;
case 34: nm = "Resilience Rating"; break;
case 36: nm = "Expertise Rating"; break;
case 37: nm = "Attack Power"; break;
case 38: nm = "Ranged Attack Power"; break;
case 45: nm = "Spell Power"; break;
case 46: nm = "Healing Power"; break;
case 49: nm = "Mana per 5 sec."; break;
default: break;
}
if (nm && es.statValue > 0)
ImGui::TextColored(green, "+%d %s", es.statValue, nm);
}
}
}
ImGui::EndTooltip();
}
} // namespace ui
} // namespace wowee

View file

@ -0,0 +1,24 @@
// evaluateMacroConditionals — convenience free function.
// Thin wrapper over MacroEvaluator with concrete adapters.
// Separate TU to avoid pulling Application/Renderer into macro_evaluator unit tests.
#include "ui/chat/macro_evaluator.hpp"
#include "ui/chat/game_state_adapter.hpp"
#include "ui/chat/input_modifier_adapter.hpp"
#include "game/game_handler.hpp"
#include "core/application.hpp"
#include "rendering/renderer.hpp"
namespace wowee { namespace ui {
std::string evaluateMacroConditionals(const std::string& rawArg,
game::GameHandler& gameHandler,
uint64_t& targetOverride) {
auto* renderer = core::Application::getInstance().getRenderer();
GameStateAdapter gs(gameHandler, renderer);
InputModifierAdapter im;
MacroEvaluator eval(gs, im);
return eval.evaluate(rawArg, targetOverride);
}
} // namespace ui
} // namespace wowee

View file

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,480 +0,0 @@
#include "ui/chat_panel.hpp"
#include "ui/ui_colors.hpp"
#include "rendering/vk_context.hpp"
#include "core/application.hpp"
#include "rendering/renderer.hpp"
#include "rendering/camera.hpp"
#include "rendering/camera_controller.hpp"
#include "audio/audio_coordinator.hpp"
#include "audio/ui_sound_manager.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_loader.hpp"
#include "pipeline/dbc_layout.hpp"
#include "game/expansion_profile.hpp"
#include "game/character.hpp"
#include "core/logger.hpp"
#include <imgui.h>
#include <imgui_internal.h>
#include "core/coordinates.hpp"
#include <algorithm>
#include <cmath>
#include <sstream>
#include <cstring>
#include <unordered_map>
namespace {
using namespace wowee::ui::colors;
constexpr auto& kColorRed = kRed;
constexpr auto& kColorBrightGreen= kBrightGreen;
constexpr auto& kColorYellow = kYellow;
} // 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
}
}
std::string ChatPanel::replaceGenderPlaceholders(const std::string& text, game::GameHandler& gameHandler) {
// Get player gender, pronouns, and name
game::Gender gender = game::Gender::NONBINARY;
std::string playerName = "Adventurer";
const auto* character = gameHandler.getActiveCharacter();
if (character) {
gender = character->gender;
if (!character->name.empty()) {
playerName = character->name;
}
}
game::Pronouns pronouns = game::Pronouns::forGender(gender);
std::string result = text;
// Helper to trim whitespace
auto trim = [](std::string& s) {
const char* ws = " \t\n\r";
size_t start = s.find_first_not_of(ws);
if (start == std::string::npos) { s.clear(); return; }
size_t end = s.find_last_not_of(ws);
s = s.substr(start, end - start + 1);
};
// Replace $g/$G placeholders first.
size_t pos = 0;
while ((pos = result.find('$', pos)) != std::string::npos) {
if (pos + 1 >= result.length()) break;
char marker = result[pos + 1];
if (marker != 'g' && marker != 'G') { pos++; continue; }
size_t endPos = result.find(';', pos);
if (endPos == std::string::npos) { pos += 2; continue; }
std::string placeholder = result.substr(pos + 2, endPos - pos - 2);
// Split by colons
std::vector<std::string> parts;
size_t start = 0;
size_t colonPos;
while ((colonPos = placeholder.find(':', start)) != std::string::npos) {
std::string part = placeholder.substr(start, colonPos - start);
trim(part);
parts.push_back(part);
start = colonPos + 1;
}
// Add the last part
std::string lastPart = placeholder.substr(start);
trim(lastPart);
parts.push_back(lastPart);
// Select appropriate text based on gender
std::string replacement;
if (parts.size() >= 3) {
// Three options: male, female, nonbinary
switch (gender) {
case game::Gender::MALE:
replacement = parts[0];
break;
case game::Gender::FEMALE:
replacement = parts[1];
break;
case game::Gender::NONBINARY:
replacement = parts[2];
break;
}
} else if (parts.size() >= 2) {
// Two options: male, female (use first for nonbinary)
switch (gender) {
case game::Gender::MALE:
replacement = parts[0];
break;
case game::Gender::FEMALE:
replacement = parts[1];
break;
case game::Gender::NONBINARY:
// Default to gender-neutral: use the shorter/simpler option
replacement = parts[0].length() <= parts[1].length() ? parts[0] : parts[1];
break;
}
} else {
// Malformed placeholder
pos = endPos + 1;
continue;
}
result.replace(pos, endPos - pos + 1, replacement);
pos += replacement.length();
}
// Resolve class and race names for $C and $R placeholders
std::string className = "Adventurer";
std::string raceName = "Unknown";
if (character) {
className = game::getClassName(character->characterClass);
raceName = game::getRaceName(character->race);
}
// Replace simple placeholders.
// $n/$N = player name, $c/$C = class name, $r/$R = race name
// $p = subject pronoun (he/she/they)
// $o = object pronoun (him/her/them)
// $s = possessive adjective (his/her/their)
// $S = possessive pronoun (his/hers/theirs)
// $b/$B = line break
pos = 0;
while ((pos = result.find('$', pos)) != std::string::npos) {
if (pos + 1 >= result.length()) break;
char code = result[pos + 1];
std::string replacement;
switch (code) {
case 'n': case 'N': replacement = playerName; break;
case 'c': case 'C': replacement = className; break;
case 'r': case 'R': replacement = raceName; break;
case 'p': replacement = pronouns.subject; break;
case 'o': replacement = pronouns.object; break;
case 's': replacement = pronouns.possessive; break;
case 'S': replacement = pronouns.possessiveP; break;
case 'b': case 'B': replacement = "\n"; break;
case 'g': case 'G': pos++; continue;
default: pos++; continue;
}
result.replace(pos, 2, replacement);
pos += replacement.length();
}
// WoW markup linebreak token.
pos = 0;
while ((pos = result.find("|n", pos)) != std::string::npos) {
result.replace(pos, 2, "\n");
pos += 1;
}
pos = 0;
while ((pos = result.find("|N", pos)) != std::string::npos) {
result.replace(pos, 2, "\n");
pos += 1;
}
return result;
}
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);
}
}
// ---- Public interface methods ----
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;
}
}
void ChatPanel::insertChatLink(const std::string& link) {
if (link.empty()) return;
size_t curLen = strlen(chatInputBuffer_);
if (curLen + link.size() + 1 < sizeof(chatInputBuffer_)) {
strncat(chatInputBuffer_, link.c_str(), sizeof(chatInputBuffer_) - curLen - 1);
chatInputMoveCursorToEnd_ = true;
refocusChatInput_ = true;
}
}
void ChatPanel::activateSlashInput() {
refocusChatInput_ = true;
chatInputBuffer_[0] = '/';
chatInputBuffer_[1] = '\0';
chatInputMoveCursorToEnd_ = true;
}
void ChatPanel::activateInput() {
refocusChatInput_ = true;
}
void ChatPanel::setWhisperTarget(const std::string& name) {
selectedChatType_ = 4; // WHISPER
strncpy(whisperTargetBuffer_, name.c_str(), sizeof(whisperTargetBuffer_) - 1);
whisperTargetBuffer_[sizeof(whisperTargetBuffer_) - 1] = '\0';
refocusChatInput_ = true;
}
ChatPanel::SlashCommands ChatPanel::consumeSlashCommands() {
SlashCommands result = slashCmds_;
slashCmds_ = {};
return result;
}
void ChatPanel::renderSettingsTab(std::function<void()> saveSettingsFn) {
ImGui::Spacing();
ImGui::Text("Appearance");
ImGui::Separator();
if (ImGui::Checkbox("Show Timestamps", &chatShowTimestamps)) {
saveSettingsFn();
}
ImGui::SetItemTooltip("Show [HH:MM] before each chat message");
const char* fontSizes[] = { "Small", "Medium", "Large" };
if (ImGui::Combo("Chat Font Size", &chatFontSize, fontSizes, 3)) {
saveSettingsFn();
}
ImGui::Spacing();
ImGui::Spacing();
ImGui::Text("Auto-Join Channels");
ImGui::Separator();
if (ImGui::Checkbox("General", &chatAutoJoinGeneral)) saveSettingsFn();
if (ImGui::Checkbox("Trade", &chatAutoJoinTrade)) saveSettingsFn();
if (ImGui::Checkbox("LocalDefense", &chatAutoJoinLocalDefense)) saveSettingsFn();
if (ImGui::Checkbox("LookingForGroup", &chatAutoJoinLFG)) saveSettingsFn();
if (ImGui::Checkbox("Local", &chatAutoJoinLocal)) saveSettingsFn();
ImGui::Spacing();
ImGui::Spacing();
ImGui::Text("Joined Channels");
ImGui::Separator();
ImGui::TextDisabled("Use /join and /leave commands in chat to manage channels.");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
if (ImGui::Button("Restore Chat Defaults", ImVec2(-1, 0))) {
restoreDefaults();
saveSettingsFn();
}
}
void ChatPanel::restoreDefaults() {
chatShowTimestamps = false;
chatFontSize = 1;
chatAutoJoinGeneral = true;
chatAutoJoinTrade = true;
chatAutoJoinLocalDefense = true;
chatAutoJoinLFG = true;
chatAutoJoinLocal = true;
}
} // namespace ui
} // namespace wowee

View file

@ -895,15 +895,16 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
chatPanel_.activateSlashInput();
}
if (!io.WantTextInput && !chatPanel_.isChatInputActive() &&
KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHAT, true)) {
KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHAT, false)) {
chatPanel_.activateInput();
}
const bool textFocus = chatPanel_.isChatInputActive() || io.WantTextInput;
// Tab targeting (when keyboard not captured by UI)
if (!io.WantCaptureKeyboard) {
// When typing in chat (or any text input), never treat keys as gameplay/UI shortcuts.
// Game hotkeys — gate on textFocus (chat/text-input active) rather than
// WantCaptureKeyboard so that toggle keys like M, C, I still work when an
// ImGui window (character panel, map, etc.) happens to have focus.
{
if (!textFocus && input.isKeyJustPressed(SDL_SCANCODE_TAB)) {
const auto& movement = gameHandler.getMovementInfo();
gameHandler.tabTarget(movement.x, movement.y, movement.z);
@ -1005,7 +1006,7 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
}
// Toggle Titles window with H (hero/title screen — no conflicting keybinding)
if (input.isKeyJustPressed(SDL_SCANCODE_H) && !ImGui::GetIO().WantCaptureKeyboard) {
if (input.isKeyJustPressed(SDL_SCANCODE_H)) {
windowManager_.showTitlesWindow_ = !windowManager_.showTitlesWindow_;
}
@ -1065,7 +1066,7 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
} else if (bar[slotIdx].type == game::ActionBarSlot::ITEM && bar[slotIdx].id != 0) {
gameHandler.useItemById(bar[slotIdx].id);
} else if (bar[slotIdx].type == game::ActionBarSlot::MACRO) {
chatPanel_.executeMacroText(gameHandler, inventoryScreen, spellbookScreen, questLogScreen, gameHandler.getMacroText(bar[slotIdx].id));
chatPanel_.executeMacroText(gameHandler, gameHandler.getMacroText(bar[slotIdx].id));
}
}
}

View file

@ -2,6 +2,7 @@
#include "ui/ui_colors.hpp"
#include "ui/inventory_screen.hpp"
#include "ui/keybinding_manager.hpp"
#include "ui/chat/chat_utils.hpp"
#include "core/application.hpp"
#include "core/input.hpp"
#include <imgui.h>
@ -10,125 +11,6 @@
namespace wowee { namespace ui {
namespace {
// Helper function to replace gender placeholders, pronouns, and name
std::string replaceGenderPlaceholders(const std::string& text, game::GameHandler& gameHandler) {
game::Gender gender = game::Gender::NONBINARY;
std::string playerName = "Adventurer";
const auto* character = gameHandler.getActiveCharacter();
if (character) {
gender = character->gender;
if (!character->name.empty()) {
playerName = character->name;
}
}
game::Pronouns pronouns = game::Pronouns::forGender(gender);
std::string result = text;
auto trim = [](std::string& s) {
const char* ws = " \t\n\r";
size_t start = s.find_first_not_of(ws);
if (start == std::string::npos) { s.clear(); return; }
size_t end = s.find_last_not_of(ws);
s = s.substr(start, end - start + 1);
};
// Replace $g placeholders
size_t pos = 0;
while ((pos = result.find('$', pos)) != std::string::npos) {
if (pos + 1 >= result.length()) break;
char marker = result[pos + 1];
if (marker != 'g' && marker != 'G') { pos++; continue; }
size_t endPos = result.find(';', pos);
if (endPos == std::string::npos) { pos += 2; continue; }
std::string placeholder = result.substr(pos + 2, endPos - pos - 2);
std::vector<std::string> parts;
size_t start = 0;
size_t colonPos;
while ((colonPos = placeholder.find(':', start)) != std::string::npos) {
std::string part = placeholder.substr(start, colonPos - start);
trim(part);
parts.push_back(part);
start = colonPos + 1;
}
std::string lastPart = placeholder.substr(start);
trim(lastPart);
parts.push_back(lastPart);
std::string replacement;
if (parts.size() >= 3) {
switch (gender) {
case game::Gender::MALE: replacement = parts[0]; break;
case game::Gender::FEMALE: replacement = parts[1]; break;
case game::Gender::NONBINARY: replacement = parts[2]; break;
}
} else if (parts.size() >= 2) {
switch (gender) {
case game::Gender::MALE: replacement = parts[0]; break;
case game::Gender::FEMALE: replacement = parts[1]; break;
case game::Gender::NONBINARY:
replacement = parts[0].length() <= parts[1].length() ? parts[0] : parts[1];
break;
}
} else {
pos = endPos + 1;
continue;
}
result.replace(pos, endPos - pos + 1, replacement);
pos += replacement.length();
}
// Resolve class and race names for $C and $R placeholders
std::string className = "Adventurer";
std::string raceName = "Unknown";
if (character) {
className = game::getClassName(character->characterClass);
raceName = game::getRaceName(character->race);
}
// Replace simple placeholders
pos = 0;
while ((pos = result.find('$', pos)) != std::string::npos) {
if (pos + 1 >= result.length()) break;
char code = result[pos + 1];
std::string replacement;
switch (code) {
case 'n': case 'N': replacement = playerName; break;
case 'c': case 'C': replacement = className; break;
case 'r': case 'R': replacement = raceName; break;
case 'p': replacement = pronouns.subject; break;
case 'o': replacement = pronouns.object; break;
case 's': replacement = pronouns.possessive; break;
case 'S': replacement = pronouns.possessiveP; break;
case 'b': case 'B': replacement = "\n"; break;
case 'g': case 'G': pos++; continue;
default: pos++; continue;
}
result.replace(pos, 2, replacement);
pos += replacement.length();
}
// WoW markup linebreak token
pos = 0;
while ((pos = result.find("|n", pos)) != std::string::npos) {
result.replace(pos, 2, "\n");
pos += 1;
}
pos = 0;
while ((pos = result.find("|N", pos)) != std::string::npos) {
result.replace(pos, 2, "\n");
pos += 1;
}
return result;
}
std::string cleanQuestTitleForUi(const std::string& raw, uint32_t questId) {
std::string s = raw;
@ -446,7 +328,7 @@ void QuestLogScreen::render(game::GameHandler& gameHandler, InventoryScreen& inv
if (lastDetailRequestQuestId_ == sel.questId) lastDetailRequestQuestId_ = 0;
questDetailQueryNoResponse_.erase(sel.questId);
ImGui::TextColored(ImVec4(0.82f, 0.9f, 1.0f, 1.0f), "Summary");
std::string processedObjectives = replaceGenderPlaceholders(sel.objectives, gameHandler);
std::string processedObjectives = chat_utils::replaceGenderPlaceholders(sel.objectives, gameHandler);
float textHeight = ImGui::GetContentRegionAvail().y * 0.45f;
if (textHeight < 120.0f) textHeight = 120.0f;
ImGui::BeginChild("QuestObjectiveText", ImVec2(0, textHeight), true);

View file

@ -4,6 +4,7 @@
// ============================================================
#include "ui/window_manager.hpp"
#include "ui/chat_panel.hpp"
#include "ui/chat/chat_utils.hpp"
#include "ui/settings_panel.hpp"
#include "ui/spellbook_screen.hpp"
#include "ui/inventory_screen.hpp"
@ -252,7 +253,7 @@ void WindowManager::renderLootWindow(game::GameHandler& gameHandler,
}
void WindowManager::renderGossipWindow(game::GameHandler& gameHandler,
ChatPanel& chatPanel) {
ChatPanel& /*chatPanel*/) {
if (!gameHandler.isGossipWindowOpen()) return;
auto* window = services_.window;
@ -330,7 +331,7 @@ void WindowManager::renderGossipWindow(game::GameHandler& gameHandler,
displayText = placeholderIt->second;
}
std::string processedText = chatPanel.replaceGenderPlaceholders(displayText, gameHandler);
std::string processedText = chat_utils::replaceGenderPlaceholders(displayText, gameHandler);
std::string label = std::string(icon) + " " + processedText;
if (ImGui::Selectable(label.c_str())) {
if (opt.text == "GOSSIP_OPTION_ARMORER") {
@ -436,11 +437,11 @@ void WindowManager::renderQuestDetailsWindow(game::GameHandler& gameHandler,
bool open = true;
const auto& quest = gameHandler.getQuestDetails();
std::string processedTitle = chatPanel.replaceGenderPlaceholders(quest.title, gameHandler);
std::string processedTitle = chat_utils::replaceGenderPlaceholders(quest.title, gameHandler);
if (ImGui::Begin(processedTitle.c_str(), &open)) {
// Quest description
if (!quest.details.empty()) {
std::string processedDetails = chatPanel.replaceGenderPlaceholders(quest.details, gameHandler);
std::string processedDetails = chat_utils::replaceGenderPlaceholders(quest.details, gameHandler);
ImGui::TextWrapped("%s", processedDetails.c_str());
}
@ -449,7 +450,7 @@ void WindowManager::renderQuestDetailsWindow(game::GameHandler& gameHandler,
ImGui::Spacing();
ImGui::Separator();
ImGui::TextColored(ui::colors::kTooltipGold, "Objectives:");
std::string processedObjectives = chatPanel.replaceGenderPlaceholders(quest.objectives, gameHandler);
std::string processedObjectives = chat_utils::replaceGenderPlaceholders(quest.objectives, gameHandler);
ImGui::TextWrapped("%s", processedObjectives.c_str());
}
@ -577,10 +578,10 @@ void WindowManager::renderQuestRequestItemsWindow(game::GameHandler& gameHandler
return total;
};
std::string processedTitle = chatPanel.replaceGenderPlaceholders(quest.title, gameHandler);
std::string processedTitle = chat_utils::replaceGenderPlaceholders(quest.title, gameHandler);
if (ImGui::Begin(processedTitle.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
if (!quest.completionText.empty()) {
std::string processedCompletionText = chatPanel.replaceGenderPlaceholders(quest.completionText, gameHandler);
std::string processedCompletionText = chat_utils::replaceGenderPlaceholders(quest.completionText, gameHandler);
ImGui::TextWrapped("%s", processedCompletionText.c_str());
}
@ -670,10 +671,10 @@ void WindowManager::renderQuestOfferRewardWindow(game::GameHandler& gameHandler,
selectedChoice = 0;
}
std::string processedTitle = chatPanel.replaceGenderPlaceholders(quest.title, gameHandler);
std::string processedTitle = chat_utils::replaceGenderPlaceholders(quest.title, gameHandler);
if (ImGui::Begin(processedTitle.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
if (!quest.rewardText.empty()) {
std::string processedRewardText = chatPanel.replaceGenderPlaceholders(quest.rewardText, gameHandler);
std::string processedRewardText = chat_utils::replaceGenderPlaceholders(quest.rewardText, gameHandler);
ImGui::TextWrapped("%s", processedRewardText.c_str());
}

View file

@ -349,6 +349,56 @@ endif()
add_test(NAME world_map_zone_metadata COMMAND test_world_map_zone_metadata)
register_test_target(test_world_map_zone_metadata)
# ── test_chat_markup_parser ──────────────────────────────────
add_executable(test_chat_markup_parser
test_chat_markup_parser.cpp
${TEST_COMMON_SOURCES}
${CMAKE_SOURCE_DIR}/src/ui/chat/chat_markup_parser.cpp
)
target_include_directories(test_chat_markup_parser PRIVATE ${TEST_INCLUDE_DIRS})
target_include_directories(test_chat_markup_parser SYSTEM PRIVATE
${TEST_SYSTEM_INCLUDE_DIRS}
${CMAKE_SOURCE_DIR}/extern/imgui
)
target_link_libraries(test_chat_markup_parser PRIVATE catch2_main)
add_test(NAME chat_markup_parser COMMAND test_chat_markup_parser)
register_test_target(test_chat_markup_parser)
# ── test_macro_evaluator ─────────────────────────────────────
add_executable(test_macro_evaluator
test_macro_evaluator.cpp
${TEST_COMMON_SOURCES}
${CMAKE_SOURCE_DIR}/src/ui/chat/macro_evaluator.cpp
)
target_include_directories(test_macro_evaluator PRIVATE ${TEST_INCLUDE_DIRS})
target_include_directories(test_macro_evaluator SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
target_link_libraries(test_macro_evaluator PRIVATE catch2_main)
add_test(NAME macro_evaluator COMMAND test_macro_evaluator)
register_test_target(test_macro_evaluator)
# ── test_chat_tab_completer ──────────────────────────────────
add_executable(test_chat_tab_completer
test_chat_tab_completer.cpp
${TEST_COMMON_SOURCES}
${CMAKE_SOURCE_DIR}/src/ui/chat/chat_tab_completer.cpp
)
target_include_directories(test_chat_tab_completer PRIVATE ${TEST_INCLUDE_DIRS})
target_include_directories(test_chat_tab_completer SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
target_link_libraries(test_chat_tab_completer PRIVATE catch2_main)
add_test(NAME chat_tab_completer COMMAND test_chat_tab_completer)
register_test_target(test_chat_tab_completer)
# ── test_gm_commands ─────────────────────────────────────────
add_executable(test_gm_commands
test_gm_commands.cpp
${TEST_COMMON_SOURCES}
)
target_include_directories(test_gm_commands PRIVATE ${TEST_INCLUDE_DIRS})
target_include_directories(test_gm_commands SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
target_link_libraries(test_gm_commands PRIVATE catch2_main)
add_test(NAME gm_commands COMMAND test_gm_commands)
register_test_target(test_gm_commands)
# ── ASAN / UBSan for test targets ────────────────────────────
if(WOWEE_ENABLE_ASAN AND NOT MSVC)
foreach(_t IN LISTS ALL_TEST_TARGETS)

View file

@ -0,0 +1,207 @@
// Tests for ChatMarkupParser — WoW markup parsing into typed segments.
// Phase 2.3 of chat_panel_ref.md.
#include <catch_amalgamated.hpp>
#include "ui/chat/chat_markup_parser.hpp"
using namespace wowee::ui;
// ── Plain text ──────────────────────────────────────────────
TEST_CASE("Plain text produces single Text segment", "[chat_markup]") {
ChatMarkupParser parser;
auto segs = parser.parse("Hello world");
REQUIRE(segs.size() == 1);
CHECK(segs[0].type == SegmentType::Text);
CHECK(segs[0].text == "Hello world");
}
TEST_CASE("Empty string produces no segments", "[chat_markup]") {
ChatMarkupParser parser;
auto segs = parser.parse("");
CHECK(segs.empty());
}
// ── Color codes ─────────────────────────────────────────────
TEST_CASE("parseWowColor extracts AARRGGBB correctly", "[chat_markup]") {
// |cFF00FF00 → alpha=1.0, red=0.0, green=1.0, blue=0.0
std::string s = "|cFF00FF00some text|r";
ImVec4 c = ChatMarkupParser::parseWowColor(s, 0);
CHECK(c.x == Catch::Approx(0.0f).margin(0.01f)); // red
CHECK(c.y == Catch::Approx(1.0f).margin(0.01f)); // green
CHECK(c.z == Catch::Approx(0.0f).margin(0.01f)); // blue
CHECK(c.w == Catch::Approx(1.0f).margin(0.01f)); // alpha
}
TEST_CASE("parseWowColor with half values", "[chat_markup]") {
// |cFF808080 → gray (128/255 ≈ 0.502)
std::string s = "|cFF808080";
ImVec4 c = ChatMarkupParser::parseWowColor(s, 0);
CHECK(c.x == Catch::Approx(0.502f).margin(0.01f));
CHECK(c.y == Catch::Approx(0.502f).margin(0.01f));
CHECK(c.z == Catch::Approx(0.502f).margin(0.01f));
}
TEST_CASE("Colored text segment: |cAARRGGBB...text...|r", "[chat_markup]") {
ChatMarkupParser parser;
auto segs = parser.parse("|cFFFF0000Red text|r");
REQUIRE(segs.size() == 1);
CHECK(segs[0].type == SegmentType::ColoredText);
CHECK(segs[0].text == "Red text");
CHECK(segs[0].color.x == Catch::Approx(1.0f).margin(0.01f)); // red
CHECK(segs[0].color.y == Catch::Approx(0.0f).margin(0.01f)); // green
CHECK(segs[0].color.z == Catch::Approx(0.0f).margin(0.01f)); // blue
}
TEST_CASE("Colored text without |r includes rest of string", "[chat_markup]") {
ChatMarkupParser parser;
auto segs = parser.parse("|cFF00FF00Green forever");
REQUIRE(segs.size() == 1);
CHECK(segs[0].type == SegmentType::ColoredText);
CHECK(segs[0].text == "Green forever");
}
TEST_CASE("Mixed plain and colored text", "[chat_markup]") {
ChatMarkupParser parser;
auto segs = parser.parse("Hello |cFFFF0000world|r!");
REQUIRE(segs.size() == 3);
CHECK(segs[0].type == SegmentType::Text);
CHECK(segs[0].text == "Hello ");
CHECK(segs[1].type == SegmentType::ColoredText);
CHECK(segs[1].text == "world");
CHECK(segs[2].type == SegmentType::Text);
CHECK(segs[2].text == "!");
}
// ── Item links ──────────────────────────────────────────────
TEST_CASE("Item link: |cFFAARRGG|Hitem:ID:...|h[Name]|h|r", "[chat_markup]") {
ChatMarkupParser parser;
std::string raw = "|cff1eff00|Hitem:19019:0:0:0:0:0:0:0:80|h[Thunderfury]|h|r";
auto segs = parser.parse(raw);
REQUIRE(segs.size() == 1);
CHECK(segs[0].type == SegmentType::ItemLink);
CHECK(segs[0].text == "Thunderfury");
CHECK(segs[0].id == 19019);
CHECK_FALSE(segs[0].rawLink.empty());
}
TEST_CASE("Bare item link without color prefix", "[chat_markup]") {
ChatMarkupParser parser;
std::string raw = "|Hitem:12345:0:0:0|h[Some Item]|h";
auto segs = parser.parse(raw);
REQUIRE(segs.size() == 1);
CHECK(segs[0].type == SegmentType::ItemLink);
CHECK(segs[0].text == "Some Item");
CHECK(segs[0].id == 12345);
}
TEST_CASE("Item link with text before and after", "[chat_markup]") {
ChatMarkupParser parser;
std::string raw = "Check this: |cff0070dd|Hitem:50000:0:0:0|h[Cool Sword]|h|r nice!";
auto segs = parser.parse(raw);
REQUIRE(segs.size() == 3);
CHECK(segs[0].type == SegmentType::Text);
CHECK(segs[0].text == "Check this: ");
CHECK(segs[1].type == SegmentType::ItemLink);
CHECK(segs[1].text == "Cool Sword");
CHECK(segs[1].id == 50000);
CHECK(segs[2].type == SegmentType::Text);
CHECK(segs[2].text == " nice!");
}
// ── Spell links ─────────────────────────────────────────────
TEST_CASE("Spell link: |Hspell:ID:RANK|h[Name]|h", "[chat_markup]") {
ChatMarkupParser parser;
std::string raw = "|cff71d5ff|Hspell:48461:0|h[Wrath]|h|r";
auto segs = parser.parse(raw);
REQUIRE(segs.size() == 1);
CHECK(segs[0].type == SegmentType::SpellLink);
CHECK(segs[0].text == "Wrath");
CHECK(segs[0].id == 48461);
}
// ── Quest links ─────────────────────────────────────────────
TEST_CASE("Quest link with level extraction", "[chat_markup]") {
ChatMarkupParser parser;
std::string raw = "|cff808080|Hquest:9876:70|h[The Last Stand]|h|r";
auto segs = parser.parse(raw);
REQUIRE(segs.size() == 1);
CHECK(segs[0].type == SegmentType::QuestLink);
CHECK(segs[0].text == "The Last Stand");
CHECK(segs[0].id == 9876);
CHECK(segs[0].extra == 70); // quest level
}
// ── Achievement links ───────────────────────────────────────
TEST_CASE("Achievement link", "[chat_markup]") {
ChatMarkupParser parser;
std::string raw = "|cffffff00|Hachievement:2136:0:0:0:0:0:0:0:0:0|h[Glory of the Hero]|h|r";
auto segs = parser.parse(raw);
REQUIRE(segs.size() == 1);
CHECK(segs[0].type == SegmentType::AchievementLink);
CHECK(segs[0].text == "Glory of the Hero");
CHECK(segs[0].id == 2136);
}
// ── URLs ────────────────────────────────────────────────────
TEST_CASE("URL is detected as Url segment", "[chat_markup]") {
ChatMarkupParser parser;
auto segs = parser.parse("Visit https://example.com for info");
REQUIRE(segs.size() == 3);
CHECK(segs[0].type == SegmentType::Text);
CHECK(segs[0].text == "Visit ");
CHECK(segs[1].type == SegmentType::Url);
CHECK(segs[1].text == "https://example.com");
CHECK(segs[2].type == SegmentType::Text);
CHECK(segs[2].text == " for info");
}
TEST_CASE("URL at end of message", "[chat_markup]") {
ChatMarkupParser parser;
auto segs = parser.parse("Link: https://example.com/path?q=1");
REQUIRE(segs.size() == 2);
CHECK(segs[1].type == SegmentType::Url);
CHECK(segs[1].text == "https://example.com/path?q=1");
}
// ── Edge cases ──────────────────────────────────────────────
TEST_CASE("Short |c without enough hex digits treated as literal", "[chat_markup]") {
ChatMarkupParser parser;
auto segs = parser.parse("|c short");
REQUIRE(segs.size() == 2);
CHECK(segs[0].type == SegmentType::Text);
CHECK(segs[0].text == "|c");
CHECK(segs[1].type == SegmentType::Text);
CHECK(segs[1].text == " short");
}
TEST_CASE("Multiple item links in one message", "[chat_markup]") {
ChatMarkupParser parser;
std::string raw =
"|cff1eff00|Hitem:100:0|h[Sword]|h|r and |cff0070dd|Hitem:200:0|h[Shield]|h|r";
auto segs = parser.parse(raw);
REQUIRE(segs.size() == 3);
CHECK(segs[0].type == SegmentType::ItemLink);
CHECK(segs[0].text == "Sword");
CHECK(segs[0].id == 100);
CHECK(segs[1].type == SegmentType::Text);
CHECK(segs[1].text == " and ");
CHECK(segs[2].type == SegmentType::ItemLink);
CHECK(segs[2].text == "Shield");
CHECK(segs[2].id == 200);
}
TEST_CASE("parseWowColor with truncated string returns white", "[chat_markup]") {
ImVec4 c = ChatMarkupParser::parseWowColor("|cFF", 0);
CHECK(c.x == 1.0f);
CHECK(c.y == 1.0f);
CHECK(c.z == 1.0f);
CHECK(c.w == 1.0f);
}

View file

@ -0,0 +1,104 @@
// Unit tests for ChatTabCompleter (Phase 5).
#include <catch_amalgamated.hpp>
#include "ui/chat/chat_tab_completer.hpp"
using wowee::ui::ChatTabCompleter;
TEST_CASE("ChatTabCompleter — initial state", "[chat][tab]") {
ChatTabCompleter tc;
CHECK_FALSE(tc.isActive());
CHECK(tc.matchCount() == 0);
CHECK(tc.getCurrentMatch().empty());
CHECK(tc.getPrefix().empty());
}
TEST_CASE("ChatTabCompleter — start with empty candidates", "[chat][tab]") {
ChatTabCompleter tc;
tc.startCompletion("foo", {});
CHECK_FALSE(tc.isActive());
CHECK(tc.matchCount() == 0);
CHECK(tc.getCurrentMatch().empty());
CHECK(tc.getPrefix() == "foo");
}
TEST_CASE("ChatTabCompleter — single candidate", "[chat][tab]") {
ChatTabCompleter tc;
tc.startCompletion("h", {"/help"});
REQUIRE(tc.isActive());
CHECK(tc.matchCount() == 1);
CHECK(tc.getCurrentMatch() == "/help");
// Cycling wraps to the same entry
tc.next();
CHECK(tc.getCurrentMatch() == "/help");
}
TEST_CASE("ChatTabCompleter — multiple candidates cycle", "[chat][tab]") {
ChatTabCompleter tc;
tc.startCompletion("s", {"/say", "/sit", "/stand"});
REQUIRE(tc.isActive());
CHECK(tc.matchCount() == 3);
CHECK(tc.getCurrentMatch() == "/say");
tc.next();
CHECK(tc.getCurrentMatch() == "/sit");
tc.next();
CHECK(tc.getCurrentMatch() == "/stand");
// Wraps around
tc.next();
CHECK(tc.getCurrentMatch() == "/say");
}
TEST_CASE("ChatTabCompleter — reset clears state", "[chat][tab]") {
ChatTabCompleter tc;
tc.startCompletion("s", {"/say", "/sit"});
REQUIRE(tc.isActive());
tc.reset();
CHECK_FALSE(tc.isActive());
CHECK(tc.matchCount() == 0);
CHECK(tc.getCurrentMatch().empty());
CHECK(tc.getPrefix().empty());
}
TEST_CASE("ChatTabCompleter — new prefix restarts", "[chat][tab]") {
ChatTabCompleter tc;
tc.startCompletion("s", {"/say", "/sit"});
tc.next(); // now at /sit
CHECK(tc.getCurrentMatch() == "/sit");
// Start new session with different prefix
tc.startCompletion("h", {"/help", "/helm"});
CHECK(tc.getPrefix() == "h");
CHECK(tc.getCurrentMatch() == "/help");
CHECK(tc.matchCount() == 2);
}
TEST_CASE("ChatTabCompleter — next on empty returns false", "[chat][tab]") {
ChatTabCompleter tc;
CHECK_FALSE(tc.next());
tc.startCompletion("x", {});
CHECK_FALSE(tc.next());
}
TEST_CASE("ChatTabCompleter — next returns true when cycling", "[chat][tab]") {
ChatTabCompleter tc;
tc.startCompletion("a", {"/afk", "/assist"});
CHECK(tc.next());
CHECK(tc.getCurrentMatch() == "/assist");
}
TEST_CASE("ChatTabCompleter — prefix is preserved after cycling", "[chat][tab]") {
ChatTabCompleter tc;
tc.startCompletion("inv", {"/invite"});
tc.next();
tc.next();
CHECK(tc.getPrefix() == "inv");
}
TEST_CASE("ChatTabCompleter — start same prefix resets index", "[chat][tab]") {
ChatTabCompleter tc;
tc.startCompletion("s", {"/say", "/sit", "/stand"});
tc.next(); // /sit
tc.next(); // /stand
// Re-start with same prefix but new candidate list
tc.startCompletion("s", {"/say", "/shout"});
CHECK(tc.getCurrentMatch() == "/say");
CHECK(tc.matchCount() == 2);
}

128
tests/test_gm_commands.cpp Normal file
View file

@ -0,0 +1,128 @@
// Tests for GM command data table and completion helpers.
#include <catch_amalgamated.hpp>
#include "ui/chat/gm_command_data.hpp"
#include <algorithm>
#include <set>
#include <string>
#include <vector>
using namespace wowee::ui;
// ---------------------------------------------------------------------------
// Data table sanity checks
// ---------------------------------------------------------------------------
TEST_CASE("GM command table is non-empty", "[gm_commands]") {
REQUIRE(kGmCommands.size() > 50);
}
TEST_CASE("GM command entries have required fields", "[gm_commands]") {
for (const auto& cmd : kGmCommands) {
INFO("command: " << cmd.name);
REQUIRE(!cmd.name.empty());
REQUIRE(!cmd.syntax.empty());
REQUIRE(!cmd.help.empty());
REQUIRE(cmd.security <= 4);
}
}
TEST_CASE("GM command names are unique", "[gm_commands]") {
std::set<std::string> seen;
for (const auto& cmd : kGmCommands) {
std::string name(cmd.name);
INFO("duplicate: " << name);
REQUIRE(seen.insert(name).second);
}
}
TEST_CASE("GM command syntax starts with dot-prefix", "[gm_commands]") {
for (const auto& cmd : kGmCommands) {
INFO("command: " << cmd.name << " syntax: " << cmd.syntax);
REQUIRE(cmd.syntax.size() > 1);
REQUIRE(cmd.syntax[0] == '.');
}
}
// ---------------------------------------------------------------------------
// Completion helper (inline, matches chat_panel.cpp logic)
// ---------------------------------------------------------------------------
static std::vector<std::string> getGmCompletions(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;
}
TEST_CASE("GM completions for '.gm' include gm subcommands", "[gm_commands]") {
auto results = getGmCompletions(".gm");
REQUIRE(!results.empty());
// Should contain .gm, .gm on, .gm off, .gm fly, etc.
REQUIRE(std::find(results.begin(), results.end(), ".gm") != results.end());
REQUIRE(std::find(results.begin(), results.end(), ".gm on") != results.end());
REQUIRE(std::find(results.begin(), results.end(), ".gm off") != results.end());
}
TEST_CASE("GM completions for '.tele' include teleport commands", "[gm_commands]") {
auto results = getGmCompletions(".tele");
REQUIRE(!results.empty());
REQUIRE(std::find(results.begin(), results.end(), ".tele") != results.end());
}
TEST_CASE("GM completions for '.add' include additem", "[gm_commands]") {
auto results = getGmCompletions(".add");
REQUIRE(!results.empty());
REQUIRE(std::find(results.begin(), results.end(), ".additem") != results.end());
}
TEST_CASE("GM completions for '.xyz' returns empty (no match)", "[gm_commands]") {
auto results = getGmCompletions(".xyz");
REQUIRE(results.empty());
}
TEST_CASE("GM completions are sorted", "[gm_commands]") {
auto results = getGmCompletions(".ch");
REQUIRE(results.size() > 1);
REQUIRE(std::is_sorted(results.begin(), results.end()));
}
TEST_CASE("GM completions for '.' returns all commands", "[gm_commands]") {
auto results = getGmCompletions(".");
REQUIRE(results.size() == kGmCommands.size());
}
TEST_CASE("Key GM commands exist in table", "[gm_commands]") {
std::set<std::string> names;
for (const auto& cmd : kGmCommands) {
names.insert(std::string(cmd.name));
}
// Check essential commands
CHECK(names.count("gm"));
CHECK(names.count("gm on"));
CHECK(names.count("tele"));
CHECK(names.count("go xyz"));
CHECK(names.count("additem"));
CHECK(names.count("levelup"));
CHECK(names.count("revive"));
CHECK(names.count("die"));
CHECK(names.count("cheat god"));
CHECK(names.count("cast"));
CHECK(names.count("learn"));
CHECK(names.count("modify money"));
CHECK(names.count("lookup item"));
CHECK(names.count("npc add"));
CHECK(names.count("ban account"));
CHECK(names.count("kick"));
CHECK(names.count("server info"));
CHECK(names.count("help"));
CHECK(names.count("commands"));
CHECK(names.count("save"));
CHECK(names.count("summon"));
CHECK(names.count("appear"));
}

View file

@ -0,0 +1,528 @@
// Tests for MacroEvaluator — WoW macro conditional parsing and evaluation.
// Phase 4.5 of chat_panel_ref.md.
#include <catch_amalgamated.hpp>
#include "ui/chat/macro_evaluator.hpp"
#include "ui/chat/i_game_state.hpp"
#include "ui/chat/i_modifier_state.hpp"
#include <unordered_map>
#include <unordered_set>
using namespace wowee::ui;
// ── Mock IGameState ─────────────────────────────────────────
class MockGameState : public IGameState {
public:
// GUIDs
uint64_t playerGuid = 1;
uint64_t targetGuid = 2;
uint64_t focusGuid = 0;
uint64_t petGuid = 0;
uint64_t mouseoverGuid = 0;
// State flags
bool inCombat = false;
bool mounted = false;
bool swimming = false;
bool flying = false;
bool casting = false;
bool channeling = false;
bool stealthed = false;
bool pet = false;
bool inGroup = false;
bool inRaid = false;
bool indoors = false;
// Numeric
uint8_t talentSpec = 0;
uint32_t vehicleId = 0;
uint32_t castSpellId = 0;
std::string castSpellName;
// Entity states (guid → exists, dead, hostile)
struct EntityInfo { bool exists = true; bool dead = false; bool hostile = false; };
std::unordered_map<uint64_t, EntityInfo> entities;
// Form / aura
bool formAura = false;
// Aura checks: map of "spellname_debuff" → true
std::unordered_set<std::string> auras;
// IGameState implementation
uint64_t getPlayerGuid() const override { return playerGuid; }
uint64_t getTargetGuid() const override { return targetGuid; }
uint64_t getFocusGuid() const override { return focusGuid; }
uint64_t getPetGuid() const override { return petGuid; }
uint64_t getMouseoverGuid() const override { return mouseoverGuid; }
bool isInCombat() const override { return inCombat; }
bool isMounted() const override { return mounted; }
bool isSwimming() const override { return swimming; }
bool isFlying() const override { return flying; }
bool isCasting() const override { return casting; }
bool isChanneling() const override { return channeling; }
bool isStealthed() const override { return stealthed; }
bool hasPet() const override { return pet; }
bool isInGroup() const override { return inGroup; }
bool isInRaid() const override { return inRaid; }
bool isIndoors() const override { return indoors; }
uint8_t getActiveTalentSpec() const override { return talentSpec; }
uint32_t getVehicleId() const override { return vehicleId; }
uint32_t getCurrentCastSpellId() const override { return castSpellId; }
std::string getSpellName(uint32_t /*spellId*/) const override { return castSpellName; }
bool hasAuraByName(uint64_t /*targetGuid*/, const std::string& spellName,
bool wantDebuff) const override {
std::string key = spellName + (wantDebuff ? "_debuff" : "_buff");
// Lowercase for comparison
for (char& c : key) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
return auras.count(key) > 0;
}
bool hasFormAura() const override { return formAura; }
bool entityExists(uint64_t guid) const override {
if (guid == 0 || guid == static_cast<uint64_t>(-1)) return false;
auto it = entities.find(guid);
return it != entities.end() && it->second.exists;
}
bool entityIsDead(uint64_t guid) const override {
auto it = entities.find(guid);
return it != entities.end() && it->second.dead;
}
bool entityIsHostile(uint64_t guid) const override {
auto it = entities.find(guid);
return it != entities.end() && it->second.hostile;
}
private:
// Need these for unordered_set/map
struct StringHash { size_t operator()(const std::string& s) const { return std::hash<std::string>{}(s); } };
};
// ── Mock IModifierState ─────────────────────────────────────
class MockModState : public IModifierState {
public:
bool shift = false;
bool ctrl = false;
bool alt = false;
bool isShiftHeld() const override { return shift; }
bool isCtrlHeld() const override { return ctrl; }
bool isAltHeld() const override { return alt; }
};
// ── Helper ──────────────────────────────────────────────────
struct TestFixture {
MockGameState gs;
MockModState ms;
MacroEvaluator eval{gs, ms};
std::string run(const std::string& input) {
uint64_t tgt;
return eval.evaluate(input, tgt);
}
std::pair<std::string, uint64_t> runWithTarget(const std::string& input) {
uint64_t tgt;
std::string result = eval.evaluate(input, tgt);
return {result, tgt};
}
};
// ── Basic parsing tests ─────────────────────────────────────
TEST_CASE("No conditionals returns input as-is", "[macro_eval]") {
TestFixture f;
CHECK(f.run("Fireball") == "Fireball");
}
TEST_CASE("Empty string returns empty", "[macro_eval]") {
TestFixture f;
CHECK(f.run("") == "");
}
TEST_CASE("Whitespace-only returns empty", "[macro_eval]") {
TestFixture f;
CHECK(f.run(" ") == "");
}
TEST_CASE("Semicolon-separated alternatives: first matches", "[macro_eval]") {
TestFixture f;
CHECK(f.run("Fireball; Frostbolt") == "Fireball");
}
TEST_CASE("Default fallback after conditions", "[macro_eval]") {
TestFixture f;
f.gs.inCombat = false;
CHECK(f.run("[combat] Attack; Heal") == "Heal");
}
// ── Combat conditions ───────────────────────────────────────
TEST_CASE("[combat] true when in combat", "[macro_eval]") {
TestFixture f;
f.gs.inCombat = true;
CHECK(f.run("[combat] Attack; Heal") == "Attack");
}
TEST_CASE("[combat] false when not in combat", "[macro_eval]") {
TestFixture f;
f.gs.inCombat = false;
CHECK(f.run("[combat] Attack; Heal") == "Heal");
}
TEST_CASE("[nocombat] true when not in combat", "[macro_eval]") {
TestFixture f;
f.gs.inCombat = false;
CHECK(f.run("[nocombat] Heal; Attack") == "Heal");
}
TEST_CASE("[nocombat] false when in combat", "[macro_eval]") {
TestFixture f;
f.gs.inCombat = true;
CHECK(f.run("[nocombat] Heal; Attack") == "Attack");
}
// ── Modifier conditions ─────────────────────────────────────
TEST_CASE("[mod:shift] true when shift held", "[macro_eval]") {
TestFixture f;
f.ms.shift = true;
CHECK(f.run("[mod:shift] Polymorph; Fireball") == "Polymorph");
}
TEST_CASE("[mod:shift] false when shift not held", "[macro_eval]") {
TestFixture f;
f.ms.shift = false;
CHECK(f.run("[mod:shift] Polymorph; Fireball") == "Fireball");
}
TEST_CASE("[mod:ctrl] true when ctrl held", "[macro_eval]") {
TestFixture f;
f.ms.ctrl = true;
CHECK(f.run("[mod:ctrl] Decurse; Fireball") == "Decurse");
}
TEST_CASE("[mod:alt] true when alt held", "[macro_eval]") {
TestFixture f;
f.ms.alt = true;
CHECK(f.run("[mod:alt] Special; Normal") == "Special");
}
TEST_CASE("[nomod] true when no modifier held", "[macro_eval]") {
TestFixture f;
CHECK(f.run("[nomod] Normal; Modified") == "Normal");
}
TEST_CASE("[nomod] false when shift held", "[macro_eval]") {
TestFixture f;
f.ms.shift = true;
CHECK(f.run("[nomod] Normal; Modified") == "Modified");
}
// ── Target specifiers ───────────────────────────────────────
TEST_CASE("[@player] sets target override to player guid", "[macro_eval]") {
TestFixture f;
f.gs.playerGuid = 42;
auto [result, tgt] = f.runWithTarget("[@player] Heal");
CHECK(result == "Heal");
CHECK(tgt == 42);
}
TEST_CASE("[@focus] sets target override to focus guid", "[macro_eval]") {
TestFixture f;
f.gs.focusGuid = 99;
auto [result, tgt] = f.runWithTarget("[@focus] Heal");
CHECK(result == "Heal");
CHECK(tgt == 99);
}
TEST_CASE("[@pet] fails when no pet", "[macro_eval]") {
TestFixture f;
f.gs.petGuid = 0;
CHECK(f.run("[@pet] Mend Pet; Steady Shot") == "Steady Shot");
}
TEST_CASE("[@pet] succeeds when pet exists", "[macro_eval]") {
TestFixture f;
f.gs.petGuid = 50;
auto [result, tgt] = f.runWithTarget("[@pet] Mend Pet; Steady Shot");
CHECK(result == "Mend Pet");
CHECK(tgt == 50);
}
TEST_CASE("[@mouseover] fails when no mouseover", "[macro_eval]") {
TestFixture f;
f.gs.mouseoverGuid = 0;
CHECK(f.run("[@mouseover] Heal; Flash Heal") == "Flash Heal");
}
TEST_CASE("[@mouseover] succeeds when mouseover exists", "[macro_eval]") {
TestFixture f;
f.gs.mouseoverGuid = 77;
auto [result, tgt] = f.runWithTarget("[@mouseover] Heal; Flash Heal");
CHECK(result == "Heal");
CHECK(tgt == 77);
}
TEST_CASE("[target=focus] sets target override", "[macro_eval]") {
TestFixture f;
f.gs.focusGuid = 33;
auto [result, tgt] = f.runWithTarget("[target=focus] Polymorph");
CHECK(result == "Polymorph");
CHECK(tgt == 33);
}
// ── Entity conditions (exists/dead/help/harm) ───────────────
TEST_CASE("[exists] true when target entity exists", "[macro_eval]") {
TestFixture f;
f.gs.targetGuid = 10;
f.gs.entities[10] = {true, false, false};
CHECK(f.run("[exists] Attack; Buff") == "Attack");
}
TEST_CASE("[exists] false when no target", "[macro_eval]") {
TestFixture f;
f.gs.targetGuid = 0;
CHECK(f.run("[exists] Attack; Buff") == "Buff");
}
TEST_CASE("[dead] true when target is dead", "[macro_eval]") {
TestFixture f;
f.gs.targetGuid = 10;
f.gs.entities[10] = {true, true, false};
CHECK(f.run("[dead] Resurrect; Heal") == "Resurrect");
}
TEST_CASE("[nodead] true when target is alive", "[macro_eval]") {
TestFixture f;
f.gs.targetGuid = 10;
f.gs.entities[10] = {true, false, false};
CHECK(f.run("[nodead] Heal; Resurrect") == "Heal");
}
TEST_CASE("[harm] true when target is hostile", "[macro_eval]") {
TestFixture f;
f.gs.targetGuid = 10;
f.gs.entities[10] = {true, false, true};
CHECK(f.run("[harm] Attack; Heal") == "Attack");
}
TEST_CASE("[help] true when target is friendly", "[macro_eval]") {
TestFixture f;
f.gs.targetGuid = 10;
f.gs.entities[10] = {true, false, false};
CHECK(f.run("[help] Heal; Attack") == "Heal");
}
// ── Chained conditions ──────────────────────────────────────
TEST_CASE("Chained conditions all must pass", "[macro_eval]") {
TestFixture f;
f.gs.inCombat = true;
f.ms.shift = true;
CHECK(f.run("[combat,mod:shift] Special; Normal") == "Special");
}
TEST_CASE("Chained conditions: one fails → whole group fails", "[macro_eval]") {
TestFixture f;
f.gs.inCombat = true;
f.ms.shift = false;
CHECK(f.run("[combat,mod:shift] Special; Normal") == "Normal");
}
// ── State conditions ────────────────────────────────────────
TEST_CASE("[mounted] true when mounted", "[macro_eval]") {
TestFixture f;
f.gs.mounted = true;
CHECK(f.run("[mounted] Dismount; Mount") == "Dismount");
}
TEST_CASE("[swimming] true when swimming", "[macro_eval]") {
TestFixture f;
f.gs.swimming = true;
CHECK(f.run("[swimming] Swim; Walk") == "Swim");
}
TEST_CASE("[flying] true when flying", "[macro_eval]") {
TestFixture f;
f.gs.flying = true;
CHECK(f.run("[flying] Land; Fly") == "Land");
}
TEST_CASE("[stealthed] true when stealthed", "[macro_eval]") {
TestFixture f;
f.gs.stealthed = true;
CHECK(f.run("[stealthed] Ambush; Sinister Strike") == "Ambush");
}
TEST_CASE("[indoors] true when indoors", "[macro_eval]") {
TestFixture f;
f.gs.indoors = true;
CHECK(f.run("[indoors] Walk; Mount") == "Walk");
}
TEST_CASE("[outdoors] true when not indoors", "[macro_eval]") {
TestFixture f;
f.gs.indoors = false;
CHECK(f.run("[outdoors] Mount; Walk") == "Mount");
}
TEST_CASE("[group] true when in group", "[macro_eval]") {
TestFixture f;
f.gs.inGroup = true;
CHECK(f.run("[group] Party Spell; Solo Spell") == "Party Spell");
}
TEST_CASE("[nogroup] true when not in group", "[macro_eval]") {
TestFixture f;
f.gs.inGroup = false;
CHECK(f.run("[nogroup] Solo; Party") == "Solo");
}
TEST_CASE("[raid] true when in raid", "[macro_eval]") {
TestFixture f;
f.gs.inRaid = true;
CHECK(f.run("[raid] Raid Spell; Normal") == "Raid Spell");
}
TEST_CASE("[pet] true when has pet", "[macro_eval]") {
TestFixture f;
f.gs.pet = true;
CHECK(f.run("[pet] Mend Pet; Steady Shot") == "Mend Pet");
}
TEST_CASE("[nopet] true when no pet", "[macro_eval]") {
TestFixture f;
f.gs.pet = false;
CHECK(f.run("[nopet] Steady Shot; Mend Pet") == "Steady Shot");
}
// ── Spec conditions ─────────────────────────────────────────
TEST_CASE("[spec:1] matches primary spec (0-based index 0)", "[macro_eval]") {
TestFixture f;
f.gs.talentSpec = 0;
CHECK(f.run("[spec:1] Heal; DPS") == "Heal");
}
TEST_CASE("[spec:2] matches secondary spec (0-based index 1)", "[macro_eval]") {
TestFixture f;
f.gs.talentSpec = 1;
CHECK(f.run("[spec:2] DPS; Heal") == "DPS");
}
TEST_CASE("[spec:1] fails when spec is secondary", "[macro_eval]") {
TestFixture f;
f.gs.talentSpec = 1;
CHECK(f.run("[spec:1] Heal; DPS") == "DPS");
}
// ── Form / stance ───────────────────────────────────────────
TEST_CASE("[noform] true when no form aura", "[macro_eval]") {
TestFixture f;
f.gs.formAura = false;
CHECK(f.run("[noform] Cast; Shift") == "Cast");
}
TEST_CASE("[noform] false when in form", "[macro_eval]") {
TestFixture f;
f.gs.formAura = true;
CHECK(f.run("[noform] Cast; Shift") == "Shift");
}
// ── Vehicle ─────────────────────────────────────────────────
TEST_CASE("[vehicle] true when in vehicle", "[macro_eval]") {
TestFixture f;
f.gs.vehicleId = 100;
CHECK(f.run("[vehicle] Vehicle Spell; Normal") == "Vehicle Spell");
}
TEST_CASE("[novehicle] true when not in vehicle", "[macro_eval]") {
TestFixture f;
f.gs.vehicleId = 0;
CHECK(f.run("[novehicle] Normal; Vehicle Spell") == "Normal");
}
// ── Casting / channeling ────────────────────────────────────
TEST_CASE("[casting] true when casting", "[macro_eval]") {
TestFixture f;
f.gs.casting = true;
CHECK(f.run("[casting] Interrupt; Cast") == "Interrupt");
}
TEST_CASE("[channeling] requires both casting and channeling", "[macro_eval]") {
TestFixture f;
f.gs.casting = true;
f.gs.channeling = true;
CHECK(f.run("[channeling] Stop; Continue") == "Stop");
}
TEST_CASE("[channeling] false when not channeling", "[macro_eval]") {
TestFixture f;
f.gs.casting = true;
f.gs.channeling = false;
CHECK(f.run("[channeling] Stop; Continue") == "Continue");
}
// ── Multiple alternatives with conditions ───────────────────
TEST_CASE("Three alternatives: first condition fails, second matches", "[macro_eval]") {
TestFixture f;
f.gs.inCombat = false;
f.gs.mounted = true;
CHECK(f.run("[combat] Attack; [mounted] Dismount; Idle") == "Dismount");
}
TEST_CASE("Three alternatives: all conditions fail, default used", "[macro_eval]") {
TestFixture f;
f.gs.inCombat = false;
f.gs.mounted = false;
CHECK(f.run("[combat] Attack; [mounted] Dismount; Idle") == "Idle");
}
TEST_CASE("No matching conditions and no default returns empty", "[macro_eval]") {
TestFixture f;
f.gs.inCombat = false;
CHECK(f.run("[combat] Attack") == "");
}
// ── Unknown conditions are permissive ───────────────────────
TEST_CASE("Unknown condition passes (permissive)", "[macro_eval]") {
TestFixture f;
CHECK(f.run("[unknowncond] Spell") == "Spell");
}
// ── targetOverride is -1 when no target specifier ───────────
TEST_CASE("No target specifier leaves override as -1", "[macro_eval]") {
TestFixture f;
auto [result, tgt] = f.runWithTarget("[combat] Spell");
CHECK(tgt == static_cast<uint64_t>(-1));
}
// ── Malformed input ──────────────────────────────────────────
TEST_CASE("Missing closing bracket skips alternative", "[macro_eval]") {
TestFixture f;
// [combat without ] → skip, then "Fallback" matches
CHECK(f.run("[combat Spell; Fallback") == "Fallback");
}
TEST_CASE("Empty brackets match (empty condition = true)", "[macro_eval]") {
TestFixture f;
CHECK(f.run("[] Spell") == "Spell");
}