mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-16 09:13:50 +00:00
Merge pull request #62 from ldmonster/chore/chat-system
[chore] refactor(chat): decompose ChatPanel into modular architecture
This commit is contained in:
commit
3c8736449a
60 changed files with 8089 additions and 4743 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 (0x00–0x1B) 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,
|
||||
// 0x24–0x26: 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,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
37
include/ui/chat/cast_sequence_tracker.hpp
Normal file
37
include/ui/chat/cast_sequence_tracker.hpp
Normal 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
|
||||
46
include/ui/chat/chat_bubble_manager.hpp
Normal file
46
include/ui/chat/chat_bubble_manager.hpp
Normal 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
|
||||
50
include/ui/chat/chat_command_registry.hpp
Normal file
50
include/ui/chat/chat_command_registry.hpp
Normal 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
|
||||
37
include/ui/chat/chat_fwd.hpp
Normal file
37
include/ui/chat/chat_fwd.hpp
Normal 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
|
||||
95
include/ui/chat/chat_input.hpp
Normal file
95
include/ui/chat/chat_input.hpp
Normal 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
|
||||
55
include/ui/chat/chat_markup_parser.hpp
Normal file
55
include/ui/chat/chat_markup_parser.hpp
Normal 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
|
||||
53
include/ui/chat/chat_markup_renderer.hpp
Normal file
53
include/ui/chat/chat_markup_renderer.hpp
Normal 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
|
||||
40
include/ui/chat/chat_settings.hpp
Normal file
40
include/ui/chat/chat_settings.hpp
Normal 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
|
||||
53
include/ui/chat/chat_tab_completer.hpp
Normal file
53
include/ui/chat/chat_tab_completer.hpp
Normal 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
|
||||
54
include/ui/chat/chat_tab_manager.hpp
Normal file
54
include/ui/chat/chat_tab_manager.hpp
Normal 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
|
||||
58
include/ui/chat/chat_utils.hpp
Normal file
58
include/ui/chat/chat_utils.hpp
Normal 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
|
||||
63
include/ui/chat/game_state_adapter.hpp
Normal file
63
include/ui/chat/game_state_adapter.hpp
Normal 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
|
||||
272
include/ui/chat/gm_command_data.hpp
Normal file
272
include/ui/chat/gm_command_data.hpp
Normal 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
|
||||
57
include/ui/chat/i_chat_command.hpp
Normal file
57
include/ui/chat/i_chat_command.hpp
Normal 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
|
||||
63
include/ui/chat/i_game_state.hpp
Normal file
63
include/ui/chat/i_game_state.hpp
Normal 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
|
||||
21
include/ui/chat/i_modifier_state.hpp
Normal file
21
include/ui/chat/i_modifier_state.hpp
Normal 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
|
||||
22
include/ui/chat/input_modifier_adapter.hpp
Normal file
22
include/ui/chat/input_modifier_adapter.hpp
Normal 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
|
||||
28
include/ui/chat/item_tooltip_renderer.hpp
Normal file
28
include/ui/chat/item_tooltip_renderer.hpp
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace wowee {
|
||||
namespace game { class GameHandler; }
|
||||
namespace pipeline { class AssetManager; }
|
||||
namespace ui {
|
||||
|
||||
class InventoryScreen;
|
||||
|
||||
/**
|
||||
* Renders a full WoW-style item tooltip via ImGui.
|
||||
*
|
||||
* Extracted from ChatMarkupRenderer::renderItemTooltip (Phase 6.7).
|
||||
* Handles: item name/quality color, armor/DPS, stats, sockets, set bonuses,
|
||||
* durability, sell price, required level, comparison tooltip.
|
||||
*/
|
||||
class ItemTooltipRenderer {
|
||||
public:
|
||||
static void render(uint32_t itemEntry,
|
||||
game::GameHandler& gameHandler,
|
||||
InventoryScreen& inventoryScreen,
|
||||
pipeline::AssetManager* assetMgr);
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
57
include/ui/chat/macro_evaluator.hpp
Normal file
57
include/ui/chat/macro_evaluator.hpp
Normal 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
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -259,6 +259,7 @@ bool MessageChatParser::parse(network::Packet& packet, MessageChatData& data) {
|
|||
|
||||
const char* getChatTypeString(ChatType type) {
|
||||
switch (type) {
|
||||
case ChatType::SYSTEM: return "SYSTEM";
|
||||
case ChatType::SAY: return "SAY";
|
||||
case ChatType::PARTY: return "PARTY";
|
||||
case ChatType::RAID: return "RAID";
|
||||
|
|
@ -266,12 +267,14 @@ const char* getChatTypeString(ChatType type) {
|
|||
case ChatType::OFFICER: return "OFFICER";
|
||||
case ChatType::YELL: return "YELL";
|
||||
case ChatType::WHISPER: return "WHISPER";
|
||||
case ChatType::WHISPER_FOREIGN: return "WHISPER_FOREIGN";
|
||||
case ChatType::WHISPER_INFORM: return "WHISPER_INFORM";
|
||||
case ChatType::EMOTE: return "EMOTE";
|
||||
case ChatType::TEXT_EMOTE: return "TEXT_EMOTE";
|
||||
case ChatType::SYSTEM: return "SYSTEM";
|
||||
case ChatType::MONSTER_SAY: return "MONSTER_SAY";
|
||||
case ChatType::MONSTER_PARTY: return "MONSTER_PARTY";
|
||||
case ChatType::MONSTER_YELL: return "MONSTER_YELL";
|
||||
case ChatType::MONSTER_WHISPER: return "MONSTER_WHISPER";
|
||||
case ChatType::MONSTER_EMOTE: return "MONSTER_EMOTE";
|
||||
case ChatType::CHANNEL: return "CHANNEL";
|
||||
case ChatType::CHANNEL_JOIN: return "CHANNEL_JOIN";
|
||||
|
|
@ -284,12 +287,18 @@ const char* getChatTypeString(ChatType type) {
|
|||
case ChatType::IGNORED: return "IGNORED";
|
||||
case ChatType::SKILL: return "SKILL";
|
||||
case ChatType::LOOT: return "LOOT";
|
||||
case ChatType::BATTLEGROUND: return "BATTLEGROUND";
|
||||
case ChatType::BATTLEGROUND_LEADER: return "BATTLEGROUND_LEADER";
|
||||
case ChatType::BG_SYSTEM_NEUTRAL: return "BG_SYSTEM_NEUTRAL";
|
||||
case ChatType::BG_SYSTEM_ALLIANCE: return "BG_SYSTEM_ALLIANCE";
|
||||
case ChatType::BG_SYSTEM_HORDE: return "BG_SYSTEM_HORDE";
|
||||
case ChatType::RAID_LEADER: return "RAID_LEADER";
|
||||
case ChatType::RAID_WARNING: return "RAID_WARNING";
|
||||
case ChatType::RAID_BOSS_EMOTE: return "RAID_BOSS_EMOTE";
|
||||
case ChatType::RAID_BOSS_WHISPER: return "RAID_BOSS_WHISPER";
|
||||
case ChatType::BATTLEGROUND: return "BATTLEGROUND";
|
||||
case ChatType::BATTLEGROUND_LEADER: return "BATTLEGROUND_LEADER";
|
||||
case ChatType::ACHIEVEMENT: return "ACHIEVEMENT";
|
||||
case ChatType::GUILD_ACHIEVEMENT: return "GUILD_ACHIEVEMENT";
|
||||
case ChatType::PARTY_LEADER: return "PARTY_LEADER";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ void ActionBarPanel::renderActionBar(game::GameHandler& gameHandler,
|
|||
ChatPanel& chatPanel,
|
||||
InventoryScreen& inventoryScreen,
|
||||
SpellbookScreen& spellbookScreen,
|
||||
QuestLogScreen& questLogScreen,
|
||||
QuestLogScreen& /*questLogScreen*/,
|
||||
SpellIconFn getSpellIcon) {
|
||||
// Use ImGui's display size — always in sync with the current swap-chain/frame,
|
||||
// whereas window->getWidth/Height() can lag by one frame on resize events.
|
||||
|
|
@ -539,7 +539,7 @@ void ActionBarPanel::renderActionBar(game::GameHandler& gameHandler,
|
|||
} else if (slot.type == game::ActionBarSlot::ITEM && slot.id != 0) {
|
||||
gameHandler.useItemById(slot.id);
|
||||
} else if (slot.type == game::ActionBarSlot::MACRO) {
|
||||
chatPanel.executeMacroText(gameHandler, inventoryScreen, spellbookScreen, questLogScreen, gameHandler.getMacroText(slot.id));
|
||||
chatPanel.executeMacroText(gameHandler, gameHandler.getMacroText(slot.id));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -572,7 +572,7 @@ void ActionBarPanel::renderActionBar(game::GameHandler& gameHandler,
|
|||
ImGui::TextDisabled("Macro #%u", slot.id);
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Execute")) {
|
||||
chatPanel.executeMacroText(gameHandler, inventoryScreen, spellbookScreen, questLogScreen, gameHandler.getMacroText(slot.id));
|
||||
chatPanel.executeMacroText(gameHandler, gameHandler.getMacroText(slot.id));
|
||||
}
|
||||
if (ImGui::MenuItem("Edit")) {
|
||||
const std::string& txt = gameHandler.getMacroText(slot.id);
|
||||
|
|
|
|||
128
src/ui/chat/chat_bubble_manager.cpp
Normal file
128
src/ui/chat/chat_bubble_manager.cpp
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
// ChatBubbleManager — 3D-projected chat bubbles above entities.
|
||||
// Moved from ChatPanel::renderBubbles / setupCallbacks (Phase 1.4).
|
||||
#include "ui/chat/chat_bubble_manager.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
#include "core/coordinates.hpp"
|
||||
#include "core/window.hpp"
|
||||
#include <imgui.h>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
void ChatBubbleManager::addBubble(uint64_t senderGuid, const std::string& message, bool isYell) {
|
||||
float duration = 8.0f + static_cast<float>(message.size()) * 0.06f;
|
||||
if (isYell) duration += 2.0f;
|
||||
if (duration > 15.0f) duration = 15.0f;
|
||||
|
||||
// Replace existing bubble for same sender
|
||||
for (auto& b : bubbles_) {
|
||||
if (b.senderGuid == senderGuid) {
|
||||
b.message = message;
|
||||
b.timeRemaining = duration;
|
||||
b.totalDuration = duration;
|
||||
b.isYell = isYell;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Evict oldest if too many
|
||||
if (bubbles_.size() >= kMaxBubbles) {
|
||||
bubbles_.erase(bubbles_.begin());
|
||||
}
|
||||
bubbles_.push_back({senderGuid, message, duration, duration, isYell});
|
||||
}
|
||||
|
||||
void ChatBubbleManager::render(game::GameHandler& gameHandler, const UIServices& services) {
|
||||
if (bubbles_.empty()) return;
|
||||
|
||||
auto* renderer = services.renderer;
|
||||
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
||||
if (!camera) return;
|
||||
|
||||
auto* window = services.window;
|
||||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
||||
|
||||
// Get delta time from ImGui
|
||||
float dt = ImGui::GetIO().DeltaTime;
|
||||
|
||||
glm::mat4 viewProj = camera->getProjectionMatrix() * camera->getViewMatrix();
|
||||
|
||||
// Update and render bubbles
|
||||
for (int i = static_cast<int>(bubbles_.size()) - 1; i >= 0; --i) {
|
||||
auto& bubble = bubbles_[i];
|
||||
bubble.timeRemaining -= dt;
|
||||
if (bubble.timeRemaining <= 0.0f) {
|
||||
bubbles_.erase(bubbles_.begin() + i);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get entity position
|
||||
auto entity = gameHandler.getEntityManager().getEntity(bubble.senderGuid);
|
||||
if (!entity) continue;
|
||||
|
||||
// Convert canonical → render coordinates, offset up by 2.5 units for bubble above head
|
||||
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ() + 2.5f);
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
|
||||
|
||||
// Project to screen
|
||||
glm::vec4 clipPos = viewProj * glm::vec4(renderPos, 1.0f);
|
||||
if (clipPos.w <= 0.0f) continue; // Behind camera
|
||||
|
||||
glm::vec2 ndc(clipPos.x / clipPos.w, clipPos.y / clipPos.w);
|
||||
float screenX = (ndc.x * 0.5f + 0.5f) * screenW;
|
||||
// Camera bakes the Vulkan Y-flip into the projection matrix:
|
||||
// NDC y=-1 is top, y=1 is bottom — same convention as nameplate/minimap projection.
|
||||
float screenY = (ndc.y * 0.5f + 0.5f) * screenH;
|
||||
|
||||
// Skip if off-screen
|
||||
if (screenX < -200.0f || screenX > screenW + 200.0f ||
|
||||
screenY < -100.0f || screenY > screenH + 100.0f) continue;
|
||||
|
||||
// Fade alpha over last 2 seconds
|
||||
float alpha = 1.0f;
|
||||
if (bubble.timeRemaining < 2.0f) {
|
||||
alpha = bubble.timeRemaining / 2.0f;
|
||||
}
|
||||
|
||||
// Draw bubble window
|
||||
std::string winId = "##ChatBubble" + std::to_string(bubble.senderGuid);
|
||||
ImGui::SetNextWindowPos(ImVec2(screenX, screenY), ImGuiCond_Always, ImVec2(0.5f, 1.0f));
|
||||
ImGui::SetNextWindowBgAlpha(0.7f * alpha);
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
||||
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoInputs |
|
||||
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 4));
|
||||
|
||||
ImGui::Begin(winId.c_str(), nullptr, flags);
|
||||
|
||||
ImVec4 textColor = bubble.isYell
|
||||
? ImVec4(1.0f, 0.2f, 0.2f, alpha)
|
||||
: ImVec4(1.0f, 1.0f, 1.0f, alpha);
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, textColor);
|
||||
ImGui::PushTextWrapPos(200.0f);
|
||||
ImGui::TextWrapped("%s", bubble.message.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar(2);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatBubbleManager::setupCallback(game::GameHandler& gameHandler) {
|
||||
if (!callbackSet_) {
|
||||
gameHandler.setChatBubbleCallback([this](uint64_t guid, const std::string& msg, bool isYell) {
|
||||
addBubble(guid, msg, isYell);
|
||||
});
|
||||
callbackSet_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
55
src/ui/chat/chat_command_registry.cpp
Normal file
55
src/ui/chat/chat_command_registry.cpp
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// ChatCommandRegistry — command registration + dispatch.
|
||||
// Moved from the if/else chain in ChatPanel::sendChatMessage() (Phase 3.1).
|
||||
#include "ui/chat/chat_command_registry.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
void ChatCommandRegistry::registerCommand(std::unique_ptr<IChatCommand> cmd) {
|
||||
IChatCommand* raw = cmd.get();
|
||||
for (const auto& alias : raw->aliases()) {
|
||||
commandMap_[alias] = raw;
|
||||
}
|
||||
commands_.push_back(std::move(cmd));
|
||||
}
|
||||
|
||||
ChatCommandResult ChatCommandRegistry::dispatch(const std::string& cmdLower,
|
||||
ChatCommandContext& ctx) {
|
||||
auto it = commandMap_.find(cmdLower);
|
||||
if (it == commandMap_.end()) {
|
||||
return {false, false}; // not handled
|
||||
}
|
||||
return it->second->execute(ctx);
|
||||
}
|
||||
|
||||
std::vector<std::string> ChatCommandRegistry::getCompletions(const std::string& prefix) const {
|
||||
std::vector<std::string> results;
|
||||
for (const auto& [alias, cmd] : commandMap_) {
|
||||
if (alias.size() >= prefix.size() &&
|
||||
alias.compare(0, prefix.size(), prefix) == 0) {
|
||||
results.push_back(alias);
|
||||
}
|
||||
}
|
||||
std::sort(results.begin(), results.end());
|
||||
return results;
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> ChatCommandRegistry::getHelpEntries() const {
|
||||
std::vector<std::pair<std::string, std::string>> entries;
|
||||
for (const auto& cmd : commands_) {
|
||||
const auto& aliases = cmd->aliases();
|
||||
std::string helpText = cmd->helpText();
|
||||
if (!aliases.empty() && !helpText.empty()) {
|
||||
entries.emplace_back("/" + aliases[0], helpText);
|
||||
}
|
||||
}
|
||||
std::sort(entries.begin(), entries.end());
|
||||
return entries;
|
||||
}
|
||||
|
||||
bool ChatCommandRegistry::hasCommand(const std::string& alias) const {
|
||||
return commandMap_.find(alias) != commandMap_.end();
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
68
src/ui/chat/chat_input.cpp
Normal file
68
src/ui/chat/chat_input.cpp
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
#include "ui/chat/chat_input.hpp"
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
// Push a message to sent-history (skip pure whitespace, cap at kMaxHistory).
|
||||
void ChatInput::pushToHistory(const std::string& msg) {
|
||||
bool allSpace = true;
|
||||
for (char c : msg) {
|
||||
if (!std::isspace(static_cast<unsigned char>(c))) { allSpace = false; break; }
|
||||
}
|
||||
if (allSpace) return;
|
||||
|
||||
// Remove duplicate of last entry if identical
|
||||
if (sentHistory_.empty() || sentHistory_.back() != msg) {
|
||||
sentHistory_.push_back(msg);
|
||||
if (static_cast<int>(sentHistory_.size()) > kMaxHistory)
|
||||
sentHistory_.erase(sentHistory_.begin());
|
||||
}
|
||||
historyIdx_ = -1; // reset browsing position after send
|
||||
}
|
||||
|
||||
// Navigate up through sent-history. Returns the entry or "" if at start.
|
||||
std::string ChatInput::historyUp() {
|
||||
const int histSize = static_cast<int>(sentHistory_.size());
|
||||
if (histSize == 0) return "";
|
||||
|
||||
if (historyIdx_ == -1)
|
||||
historyIdx_ = histSize - 1;
|
||||
else if (historyIdx_ > 0)
|
||||
--historyIdx_;
|
||||
|
||||
return sentHistory_[historyIdx_];
|
||||
}
|
||||
|
||||
// Navigate down through sent-history. Returns the entry or "" if past end.
|
||||
std::string ChatInput::historyDown() {
|
||||
const int histSize = static_cast<int>(sentHistory_.size());
|
||||
if (histSize == 0 || historyIdx_ == -1) return "";
|
||||
|
||||
++historyIdx_;
|
||||
if (historyIdx_ >= histSize) {
|
||||
historyIdx_ = -1;
|
||||
return "";
|
||||
}
|
||||
return sentHistory_[historyIdx_];
|
||||
}
|
||||
|
||||
// Insert a spell / item link into the chat input buffer (shift-click).
|
||||
void ChatInput::insertLink(const std::string& link) {
|
||||
if (link.empty()) return;
|
||||
size_t curLen = std::strlen(buffer_);
|
||||
if (curLen + link.size() + 1 < sizeof(buffer_)) {
|
||||
strncat(buffer_, link.c_str(), sizeof(buffer_) - curLen - 1);
|
||||
moveCursorToEnd_ = true;
|
||||
focusRequested_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Activate the input field with a leading '/' (slash key).
|
||||
void ChatInput::activateSlashInput() {
|
||||
focusRequested_ = true;
|
||||
buffer_[0] = '/';
|
||||
buffer_[1] = '\0';
|
||||
moveCursorToEnd_ = true;
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
206
src/ui/chat/chat_markup_parser.cpp
Normal file
206
src/ui/chat/chat_markup_parser.cpp
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
// ChatMarkupParser — parse WoW markup into typed segments.
|
||||
// Moved from inline lambdas in ChatPanel::render() (Phase 2.1).
|
||||
#include "ui/chat/chat_markup_parser.hpp"
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
ImVec4 ChatMarkupParser::parseWowColor(const std::string& text, size_t pos) {
|
||||
// |cAARRGGBB (10 chars total: |c + 8 hex)
|
||||
if (pos + 10 > text.size()) return ImVec4(1, 1, 1, 1);
|
||||
auto hexByte = [&](size_t offset) -> float {
|
||||
const char* s = text.c_str() + pos + offset;
|
||||
char buf[3] = {s[0], s[1], '\0'};
|
||||
return static_cast<float>(strtol(buf, nullptr, 16)) / 255.0f;
|
||||
};
|
||||
float a = hexByte(2);
|
||||
float r = hexByte(4);
|
||||
float g = hexByte(6);
|
||||
float b = hexByte(8);
|
||||
return ImVec4(r, g, b, a);
|
||||
}
|
||||
|
||||
std::vector<ChatSegment> ChatMarkupParser::parse(const std::string& text) const {
|
||||
std::vector<ChatSegment> segments;
|
||||
size_t pos = 0;
|
||||
|
||||
while (pos < text.size()) {
|
||||
// Find next special element: URL or WoW link
|
||||
size_t urlStart = text.find("https://", pos);
|
||||
|
||||
// Find next WoW link (may be colored with |c prefix or bare |H)
|
||||
size_t linkStart = text.find("|c", pos);
|
||||
// Also handle bare |H links without color prefix
|
||||
size_t bareItem = text.find("|Hitem:", pos);
|
||||
size_t bareSpell = text.find("|Hspell:", pos);
|
||||
size_t bareQuest = text.find("|Hquest:", pos);
|
||||
size_t bareLinkStart = std::min({bareItem, bareSpell, bareQuest});
|
||||
|
||||
// Determine which comes first
|
||||
size_t nextSpecial = std::min({urlStart, linkStart, bareLinkStart});
|
||||
|
||||
if (nextSpecial == std::string::npos) {
|
||||
// No more special elements — remainder is plain text
|
||||
std::string remaining = text.substr(pos);
|
||||
if (!remaining.empty()) {
|
||||
segments.push_back({SegmentType::Text, std::move(remaining)});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Emit plain text before the special element
|
||||
if (nextSpecial > pos) {
|
||||
segments.push_back({SegmentType::Text, text.substr(pos, nextSpecial - pos)});
|
||||
}
|
||||
|
||||
// Handle WoW link (|c... or bare |H...)
|
||||
if (nextSpecial == linkStart || nextSpecial == bareLinkStart) {
|
||||
ImVec4 linkColor(1, 1, 1, 1);
|
||||
size_t hStart = std::string::npos;
|
||||
|
||||
if (nextSpecial == linkStart && text.size() > linkStart + 10) {
|
||||
// Parse |cAARRGGBB color
|
||||
linkColor = parseWowColor(text, linkStart);
|
||||
// Find the nearest |H link of any supported type
|
||||
size_t hItem = text.find("|Hitem:", linkStart + 10);
|
||||
size_t hSpell = text.find("|Hspell:", linkStart + 10);
|
||||
size_t hQuest = text.find("|Hquest:", linkStart + 10);
|
||||
size_t hAch = text.find("|Hachievement:", linkStart + 10);
|
||||
hStart = std::min({hItem, hSpell, hQuest, hAch});
|
||||
} else if (nextSpecial == bareLinkStart) {
|
||||
hStart = bareLinkStart;
|
||||
}
|
||||
|
||||
if (hStart != std::string::npos) {
|
||||
// Determine link type
|
||||
const bool isSpellLink = (text.compare(hStart, 8, "|Hspell:") == 0);
|
||||
const bool isQuestLink = (text.compare(hStart, 8, "|Hquest:") == 0);
|
||||
const bool isAchievLink = (text.compare(hStart, 14, "|Hachievement:") == 0);
|
||||
|
||||
// Parse the first numeric ID after |Htype:
|
||||
size_t idOffset = isSpellLink ? 8 : (isQuestLink ? 8 : (isAchievLink ? 14 : 7));
|
||||
size_t entryStart = hStart + idOffset;
|
||||
size_t entryEnd = text.find(':', entryStart);
|
||||
uint32_t linkId = 0;
|
||||
if (entryEnd != std::string::npos) {
|
||||
linkId = static_cast<uint32_t>(strtoul(
|
||||
text.substr(entryStart, entryEnd - entryStart).c_str(), nullptr, 10));
|
||||
}
|
||||
|
||||
// Parse quest level (second field after questId)
|
||||
uint32_t questLevel = 0;
|
||||
if (isQuestLink && entryEnd != std::string::npos) {
|
||||
size_t lvlEnd = text.find(':', entryEnd + 1);
|
||||
if (lvlEnd == std::string::npos) lvlEnd = text.find('|', entryEnd + 1);
|
||||
if (lvlEnd != std::string::npos) {
|
||||
questLevel = static_cast<uint32_t>(strtoul(
|
||||
text.substr(entryEnd + 1, lvlEnd - entryEnd - 1).c_str(), nullptr, 10));
|
||||
}
|
||||
}
|
||||
|
||||
// Find display name: |h[Name]|h
|
||||
size_t nameTagStart = text.find("|h[", hStart);
|
||||
size_t nameTagEnd = (nameTagStart != std::string::npos)
|
||||
? text.find("]|h", nameTagStart + 3) : std::string::npos;
|
||||
|
||||
std::string linkName = isSpellLink ? "Unknown Spell"
|
||||
: isQuestLink ? "Unknown Quest"
|
||||
: isAchievLink ? "Unknown Achievement"
|
||||
: "Unknown Item";
|
||||
if (nameTagStart != std::string::npos && nameTagEnd != std::string::npos) {
|
||||
linkName = text.substr(nameTagStart + 3, nameTagEnd - nameTagStart - 3);
|
||||
}
|
||||
|
||||
// Find end of entire link sequence (|r or after ]|h)
|
||||
size_t linkEnd = (nameTagEnd != std::string::npos) ? nameTagEnd + 3 : hStart + idOffset;
|
||||
size_t resetPos = text.find("|r", linkEnd);
|
||||
if (resetPos != std::string::npos && resetPos <= linkEnd + 2) {
|
||||
linkEnd = resetPos + 2;
|
||||
}
|
||||
|
||||
// Build raw link text for shift-click re-insertion
|
||||
std::string rawLink = text.substr(nextSpecial, linkEnd - nextSpecial);
|
||||
|
||||
// Emit appropriate segment type
|
||||
SegmentType stype = isSpellLink ? SegmentType::SpellLink
|
||||
: isQuestLink ? SegmentType::QuestLink
|
||||
: isAchievLink ? SegmentType::AchievementLink
|
||||
: SegmentType::ItemLink;
|
||||
|
||||
ChatSegment seg;
|
||||
seg.type = stype;
|
||||
seg.text = std::move(linkName);
|
||||
seg.color = linkColor;
|
||||
seg.id = linkId;
|
||||
seg.extra = questLevel;
|
||||
seg.rawLink = std::move(rawLink);
|
||||
segments.push_back(std::move(seg));
|
||||
|
||||
pos = linkEnd;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not an item link — treat as colored text: |cAARRGGBB...text...|r
|
||||
if (nextSpecial == linkStart && text.size() > linkStart + 10) {
|
||||
ImVec4 cColor = parseWowColor(text, linkStart);
|
||||
size_t textStart = linkStart + 10; // after |cAARRGGBB
|
||||
size_t resetPos2 = text.find("|r", textStart);
|
||||
std::string coloredText;
|
||||
if (resetPos2 != std::string::npos) {
|
||||
coloredText = text.substr(textStart, resetPos2 - textStart);
|
||||
pos = resetPos2 + 2; // skip |r
|
||||
} else {
|
||||
coloredText = text.substr(textStart);
|
||||
pos = text.size();
|
||||
}
|
||||
// Strip any remaining WoW markup from the colored segment
|
||||
// (e.g. |H...|h pairs that aren't item links)
|
||||
std::string clean;
|
||||
for (size_t i = 0; i < coloredText.size(); i++) {
|
||||
if (coloredText[i] == '|' && i + 1 < coloredText.size()) {
|
||||
char next = coloredText[i + 1];
|
||||
if (next == 'H') {
|
||||
// Skip |H...|h
|
||||
size_t hEnd = coloredText.find("|h", i + 2);
|
||||
if (hEnd != std::string::npos) { i = hEnd + 1; continue; }
|
||||
} else if (next == 'h') {
|
||||
i += 1; continue; // skip |h
|
||||
} else if (next == 'r') {
|
||||
i += 1; continue; // skip |r
|
||||
}
|
||||
}
|
||||
clean += coloredText[i];
|
||||
}
|
||||
if (!clean.empty()) {
|
||||
ChatSegment seg;
|
||||
seg.type = SegmentType::ColoredText;
|
||||
seg.text = std::move(clean);
|
||||
seg.color = cColor;
|
||||
segments.push_back(std::move(seg));
|
||||
}
|
||||
} else {
|
||||
// Bare |c without enough chars for color — render literally
|
||||
segments.push_back({SegmentType::Text, "|c"});
|
||||
pos = nextSpecial + 2;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle URL
|
||||
if (nextSpecial == urlStart) {
|
||||
size_t urlEnd = text.find_first_of(" \t\n\r", urlStart);
|
||||
if (urlEnd == std::string::npos) urlEnd = text.size();
|
||||
std::string url = text.substr(urlStart, urlEnd - urlStart);
|
||||
|
||||
segments.push_back({SegmentType::Url, std::move(url)});
|
||||
pos = urlEnd;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
192
src/ui/chat/chat_markup_renderer.cpp
Normal file
192
src/ui/chat/chat_markup_renderer.cpp
Normal 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
|
||||
63
src/ui/chat/chat_settings.cpp
Normal file
63
src/ui/chat/chat_settings.cpp
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
#include "ui/chat/chat_settings.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
// Reset all chat settings to defaults.
|
||||
void ChatSettings::restoreDefaults() {
|
||||
showTimestamps = false;
|
||||
fontSize = 1;
|
||||
autoJoinGeneral = true;
|
||||
autoJoinTrade = true;
|
||||
autoJoinLocalDefense = true;
|
||||
autoJoinLFG = true;
|
||||
autoJoinLocal = true;
|
||||
}
|
||||
|
||||
// Render the "Chat" tab inside the Settings window.
|
||||
void ChatSettings::renderSettingsTab(std::function<void()> saveSettingsFn) {
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Appearance");
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::Checkbox("Show Timestamps", &showTimestamps)) {
|
||||
saveSettingsFn();
|
||||
}
|
||||
ImGui::SetItemTooltip("Show [HH:MM] before each chat message");
|
||||
|
||||
const char* fontSizes[] = { "Small", "Medium", "Large" };
|
||||
if (ImGui::Combo("Chat Font Size", &fontSize, fontSizes, 3)) {
|
||||
saveSettingsFn();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("Auto-Join Channels");
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::Checkbox("General", &autoJoinGeneral)) saveSettingsFn();
|
||||
if (ImGui::Checkbox("Trade", &autoJoinTrade)) saveSettingsFn();
|
||||
if (ImGui::Checkbox("LocalDefense", &autoJoinLocalDefense)) saveSettingsFn();
|
||||
if (ImGui::Checkbox("LookingForGroup", &autoJoinLFG)) saveSettingsFn();
|
||||
if (ImGui::Checkbox("Local", &autoJoinLocal)) saveSettingsFn();
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("Joined Channels");
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::TextDisabled("Use /join and /leave commands in chat to manage channels.");
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
if (ImGui::Button("Restore Chat Defaults", ImVec2(-1, 0))) {
|
||||
restoreDefaults();
|
||||
saveSettingsFn();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
35
src/ui/chat/chat_tab_completer.cpp
Normal file
35
src/ui/chat/chat_tab_completer.cpp
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// ChatTabCompleter — cycling tab-completion state machine.
|
||||
// Extracted from ChatPanel tab-completion logic (Phase 5.1).
|
||||
#include "ui/chat/chat_tab_completer.hpp"
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
void ChatTabCompleter::startCompletion(const std::string& prefix,
|
||||
std::vector<std::string> candidates) {
|
||||
prefix_ = prefix;
|
||||
matches_ = std::move(candidates);
|
||||
matchIdx_ = matches_.empty() ? -1 : 0;
|
||||
}
|
||||
|
||||
bool ChatTabCompleter::next() {
|
||||
if (matches_.empty()) return false;
|
||||
++matchIdx_;
|
||||
if (matchIdx_ >= static_cast<int>(matches_.size()))
|
||||
matchIdx_ = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string ChatTabCompleter::getCurrentMatch() const {
|
||||
if (matchIdx_ < 0 || matchIdx_ >= static_cast<int>(matches_.size()))
|
||||
return "";
|
||||
return matches_[matchIdx_];
|
||||
}
|
||||
|
||||
void ChatTabCompleter::reset() {
|
||||
prefix_.clear();
|
||||
matches_.clear();
|
||||
matchIdx_ = -1;
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
198
src/ui/chat/chat_tab_manager.cpp
Normal file
198
src/ui/chat/chat_tab_manager.cpp
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
#include "ui/chat/chat_tab_manager.hpp"
|
||||
#include "ui/ui_colors.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
namespace {
|
||||
using namespace wowee::ui::colors;
|
||||
} // namespace
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
ChatTabManager::ChatTabManager() {
|
||||
initTabs();
|
||||
}
|
||||
|
||||
void ChatTabManager::initTabs() {
|
||||
tabs_.clear();
|
||||
// General tab: shows everything
|
||||
tabs_.push_back({"General", ~0ULL});
|
||||
// Combat tab: system, loot, skills, achievements, and NPC speech/emotes
|
||||
tabs_.push_back({"Combat", (1ULL << static_cast<uint8_t>(game::ChatType::SYSTEM)) |
|
||||
(1ULL << static_cast<uint8_t>(game::ChatType::LOOT)) |
|
||||
(1ULL << static_cast<uint8_t>(game::ChatType::SKILL)) |
|
||||
(1ULL << static_cast<uint8_t>(game::ChatType::ACHIEVEMENT)) |
|
||||
(1ULL << static_cast<uint8_t>(game::ChatType::GUILD_ACHIEVEMENT)) |
|
||||
(1ULL << static_cast<uint8_t>(game::ChatType::MONSTER_SAY)) |
|
||||
(1ULL << static_cast<uint8_t>(game::ChatType::MONSTER_YELL)) |
|
||||
(1ULL << static_cast<uint8_t>(game::ChatType::MONSTER_EMOTE)) |
|
||||
(1ULL << static_cast<uint8_t>(game::ChatType::MONSTER_WHISPER)) |
|
||||
(1ULL << static_cast<uint8_t>(game::ChatType::MONSTER_PARTY)) |
|
||||
(1ULL << static_cast<uint8_t>(game::ChatType::RAID_BOSS_WHISPER)) |
|
||||
(1ULL << static_cast<uint8_t>(game::ChatType::RAID_BOSS_EMOTE))});
|
||||
// Whispers tab
|
||||
tabs_.push_back({"Whispers", (1ULL << static_cast<uint8_t>(game::ChatType::WHISPER)) |
|
||||
(1ULL << static_cast<uint8_t>(game::ChatType::WHISPER_INFORM))});
|
||||
// Guild tab: guild and officer chat
|
||||
tabs_.push_back({"Guild", (1ULL << static_cast<uint8_t>(game::ChatType::GUILD)) |
|
||||
(1ULL << static_cast<uint8_t>(game::ChatType::OFFICER)) |
|
||||
(1ULL << static_cast<uint8_t>(game::ChatType::GUILD_ACHIEVEMENT))});
|
||||
// Trade/LFG tab: channel messages
|
||||
tabs_.push_back({"Trade/LFG", (1ULL << static_cast<uint8_t>(game::ChatType::CHANNEL))});
|
||||
|
||||
unread_.assign(tabs_.size(), 0);
|
||||
seenCount_ = 0;
|
||||
}
|
||||
|
||||
int ChatTabManager::getUnreadCount(int idx) const {
|
||||
if (idx < 0 || idx >= static_cast<int>(unread_.size())) return 0;
|
||||
return unread_[idx];
|
||||
}
|
||||
|
||||
void ChatTabManager::clearUnread(int idx) {
|
||||
if (idx >= 0 && idx < static_cast<int>(unread_.size()))
|
||||
unread_[idx] = 0;
|
||||
}
|
||||
|
||||
void ChatTabManager::updateUnread(const std::deque<game::MessageChatData>& history, int activeTab) {
|
||||
// Ensure unread array is sized correctly (guards against late init)
|
||||
if (unread_.size() != tabs_.size())
|
||||
unread_.assign(tabs_.size(), 0);
|
||||
// If history shrank (e.g. cleared), reset
|
||||
if (seenCount_ > history.size()) seenCount_ = 0;
|
||||
for (size_t mi = seenCount_; mi < history.size(); ++mi) {
|
||||
const auto& msg = history[mi];
|
||||
// For each non-General (non-0) tab that isn't currently active, check visibility
|
||||
for (int ti = 1; ti < static_cast<int>(tabs_.size()); ++ti) {
|
||||
if (ti == activeTab) continue;
|
||||
if (shouldShowMessage(msg, ti)) {
|
||||
unread_[ti]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
seenCount_ = history.size();
|
||||
}
|
||||
|
||||
bool ChatTabManager::shouldShowMessage(const game::MessageChatData& msg, int tabIndex) const {
|
||||
if (tabIndex < 0 || tabIndex >= static_cast<int>(tabs_.size())) return true;
|
||||
const auto& tab = tabs_[tabIndex];
|
||||
if (tab.typeMask == ~0ULL) return true; // General tab shows all
|
||||
|
||||
uint64_t typeBit = 1ULL << static_cast<uint8_t>(msg.type);
|
||||
|
||||
// For Trade/LFG tab (index 4), also filter by channel name
|
||||
if (tabIndex == 4 && msg.type == game::ChatType::CHANNEL) {
|
||||
const std::string& ch = msg.channelName;
|
||||
if (ch.find("Trade") == std::string::npos &&
|
||||
ch.find("General") == std::string::npos &&
|
||||
ch.find("LookingForGroup") == std::string::npos &&
|
||||
ch.find("Local") == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return (tab.typeMask & typeBit) != 0;
|
||||
}
|
||||
|
||||
// ---- Static chat type helpers ----
|
||||
|
||||
const char* ChatTabManager::getChatTypeName(game::ChatType type) {
|
||||
switch (type) {
|
||||
case game::ChatType::SAY: return "Say";
|
||||
case game::ChatType::YELL: return "Yell";
|
||||
case game::ChatType::EMOTE: return "Emote";
|
||||
case game::ChatType::TEXT_EMOTE: return "Emote";
|
||||
case game::ChatType::PARTY: return "Party";
|
||||
case game::ChatType::GUILD: return "Guild";
|
||||
case game::ChatType::OFFICER: return "Officer";
|
||||
case game::ChatType::RAID: return "Raid";
|
||||
case game::ChatType::RAID_LEADER: return "Raid Leader";
|
||||
case game::ChatType::RAID_WARNING: return "Raid Warning";
|
||||
case game::ChatType::BATTLEGROUND: return "Battleground";
|
||||
case game::ChatType::BATTLEGROUND_LEADER: return "Battleground Leader";
|
||||
case game::ChatType::WHISPER: return "Whisper";
|
||||
case game::ChatType::WHISPER_INFORM: return "To";
|
||||
case game::ChatType::SYSTEM: return "System";
|
||||
case game::ChatType::MONSTER_SAY: return "Say";
|
||||
case game::ChatType::MONSTER_YELL: return "Yell";
|
||||
case game::ChatType::MONSTER_EMOTE: return "Emote";
|
||||
case game::ChatType::CHANNEL: return "Channel";
|
||||
case game::ChatType::ACHIEVEMENT: return "Achievement";
|
||||
case game::ChatType::DND: return "DND";
|
||||
case game::ChatType::AFK: return "AFK";
|
||||
case game::ChatType::BG_SYSTEM_NEUTRAL:
|
||||
case game::ChatType::BG_SYSTEM_ALLIANCE:
|
||||
case game::ChatType::BG_SYSTEM_HORDE: return "System";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
ImVec4 ChatTabManager::getChatTypeColor(game::ChatType type) {
|
||||
switch (type) {
|
||||
case game::ChatType::SAY:
|
||||
return kWhite;
|
||||
case game::ChatType::YELL:
|
||||
return kRed;
|
||||
case game::ChatType::EMOTE:
|
||||
case game::ChatType::TEXT_EMOTE:
|
||||
return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange
|
||||
case game::ChatType::PARTY:
|
||||
return ImVec4(0.5f, 0.5f, 1.0f, 1.0f); // Light blue
|
||||
case game::ChatType::GUILD:
|
||||
return kBrightGreen;
|
||||
case game::ChatType::OFFICER:
|
||||
return ImVec4(0.3f, 0.8f, 0.3f, 1.0f); // Dark green
|
||||
case game::ChatType::RAID:
|
||||
return ImVec4(1.0f, 0.5f, 0.0f, 1.0f); // Orange
|
||||
case game::ChatType::RAID_LEADER:
|
||||
return ImVec4(1.0f, 0.4f, 0.0f, 1.0f); // Darker orange
|
||||
case game::ChatType::RAID_WARNING:
|
||||
return ImVec4(1.0f, 0.0f, 0.0f, 1.0f); // Red
|
||||
case game::ChatType::BATTLEGROUND:
|
||||
return ImVec4(1.0f, 0.6f, 0.0f, 1.0f); // Orange-gold
|
||||
case game::ChatType::BATTLEGROUND_LEADER:
|
||||
return ImVec4(1.0f, 0.5f, 0.0f, 1.0f); // Orange
|
||||
case game::ChatType::WHISPER:
|
||||
case game::ChatType::WHISPER_INFORM:
|
||||
return ImVec4(1.0f, 0.5f, 1.0f, 1.0f); // Pink
|
||||
case game::ChatType::SYSTEM:
|
||||
return kYellow;
|
||||
case game::ChatType::MONSTER_SAY:
|
||||
return kWhite;
|
||||
case game::ChatType::MONSTER_YELL:
|
||||
return kRed;
|
||||
case game::ChatType::MONSTER_EMOTE:
|
||||
return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange
|
||||
case game::ChatType::CHANNEL:
|
||||
return ImVec4(1.0f, 0.7f, 0.7f, 1.0f); // Light pink
|
||||
case game::ChatType::ACHIEVEMENT:
|
||||
return ImVec4(1.0f, 1.0f, 0.0f, 1.0f); // Bright yellow
|
||||
case game::ChatType::GUILD_ACHIEVEMENT:
|
||||
return kWarmGold;
|
||||
case game::ChatType::SKILL:
|
||||
return kCyan;
|
||||
case game::ChatType::LOOT:
|
||||
return ImVec4(0.8f, 0.5f, 1.0f, 1.0f); // Light purple
|
||||
case game::ChatType::MONSTER_WHISPER:
|
||||
case game::ChatType::RAID_BOSS_WHISPER:
|
||||
return ImVec4(1.0f, 0.5f, 1.0f, 1.0f); // Pink
|
||||
case game::ChatType::RAID_BOSS_EMOTE:
|
||||
return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange
|
||||
case game::ChatType::MONSTER_PARTY:
|
||||
return ImVec4(0.5f, 0.5f, 1.0f, 1.0f); // Light blue
|
||||
case game::ChatType::BG_SYSTEM_NEUTRAL:
|
||||
return kWarmGold;
|
||||
case game::ChatType::BG_SYSTEM_ALLIANCE:
|
||||
return ImVec4(0.3f, 0.6f, 1.0f, 1.0f); // Blue
|
||||
case game::ChatType::BG_SYSTEM_HORDE:
|
||||
return kRed;
|
||||
case game::ChatType::AFK:
|
||||
case game::ChatType::DND:
|
||||
return ImVec4(0.85f, 0.85f, 0.85f, 0.8f); // Light gray
|
||||
default:
|
||||
return kLightGray;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
157
src/ui/chat/chat_utils.cpp
Normal file
157
src/ui/chat/chat_utils.cpp
Normal 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
|
||||
341
src/ui/chat/commands/channel_commands.cpp
Normal file
341
src/ui/chat/commands/channel_commands.cpp
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
// Channel commands: /s, /y, /p, /g, /raid, /rw, /o, /bg, /i, /join, /leave, /wts, /wtb, /1-9, /w, /r
|
||||
// Moved from ChatPanel::sendChatMessage() channel dispatch section (Phase 3).
|
||||
// These commands send messages to specific chat channels and/or switch the
|
||||
// chat-type dropdown on the panel.
|
||||
#include "ui/chat/i_chat_command.hpp"
|
||||
#include "ui/chat_panel.hpp"
|
||||
#include "ui/chat/chat_utils.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
using wowee::ui::chat_utils::trim;
|
||||
using wowee::ui::chat_utils::toLower;
|
||||
|
||||
namespace {
|
||||
|
||||
bool isPortBotTarget(const std::string& target) {
|
||||
std::string t = toLower(trim(target));
|
||||
return t == "portbot" || t == "gmbot" || t == "telebot";
|
||||
}
|
||||
|
||||
std::string buildPortBotCommand(const std::string& rawInput) {
|
||||
std::string input = trim(rawInput);
|
||||
if (input.empty()) return "";
|
||||
std::string lower = toLower(input);
|
||||
if (lower == "help" || lower == "?") return "__help__";
|
||||
if (lower.rfind(".tele ", 0) == 0 || lower.rfind(".go ", 0) == 0) return input;
|
||||
if (lower.rfind("xyz ", 0) == 0) return ".go " + input;
|
||||
if (lower == "sw" || lower == "stormwind") return ".tele stormwind";
|
||||
if (lower == "if" || lower == "ironforge") return ".tele ironforge";
|
||||
if (lower == "darn" || lower == "darnassus") return ".tele darnassus";
|
||||
if (lower == "org" || lower == "orgrimmar") return ".tele orgrimmar";
|
||||
if (lower == "tb" || lower == "thunderbluff") return ".tele thunderbluff";
|
||||
if (lower == "uc" || lower == "undercity") return ".tele undercity";
|
||||
if (lower == "shatt" || lower == "shattrath") return ".tele shattrath";
|
||||
if (lower == "dal" || lower == "dalaran") return ".tele dalaran";
|
||||
return ".tele " + input;
|
||||
}
|
||||
|
||||
// Send a whisper, intercepting PortBot targets for GM teleport commands.
|
||||
// Returns true if the whisper was handled (PortBot or normal send), false if empty.
|
||||
bool sendWhisperOrPortBot(wowee::game::GameHandler& gameHandler,
|
||||
const std::string& target,
|
||||
const std::string& message) {
|
||||
if (isPortBotTarget(target)) {
|
||||
std::string cmd = buildPortBotCommand(message);
|
||||
wowee::game::MessageChatData msg;
|
||||
msg.type = wowee::game::ChatType::SYSTEM;
|
||||
msg.language = wowee::game::ChatLanguage::UNIVERSAL;
|
||||
if (cmd.empty() || cmd == "__help__") {
|
||||
msg.message = "PortBot: /w PortBot <dest>. Aliases: sw if darn org tb uc shatt dal. Also supports '.tele ...' or 'xyz x y z [map [o]]'.";
|
||||
gameHandler.addLocalChatMessage(msg);
|
||||
return true;
|
||||
}
|
||||
gameHandler.sendChatMessage(wowee::game::ChatType::SAY, cmd, "");
|
||||
msg.message = "PortBot executed: " + cmd;
|
||||
gameHandler.addLocalChatMessage(msg);
|
||||
return true;
|
||||
}
|
||||
if (!message.empty()) {
|
||||
gameHandler.sendChatMessage(wowee::game::ChatType::WHISPER, message, target);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
// --- Helper: send a message via a specific chat type + optionally switch dropdown ---
|
||||
static ChatCommandResult sendAndSwitch(ChatCommandContext& ctx,
|
||||
game::ChatType chatType,
|
||||
int switchIdx,
|
||||
const std::string& target = "") {
|
||||
if (!ctx.args.empty())
|
||||
ctx.gameHandler.sendChatMessage(chatType, ctx.args, target);
|
||||
ctx.panel.setSelectedChatType(switchIdx);
|
||||
return {};
|
||||
}
|
||||
|
||||
// --- /s, /say ---
|
||||
class SayCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
return sendAndSwitch(ctx, game::ChatType::SAY, 0);
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"s", "say"}; }
|
||||
std::string helpText() const override { return "Say to nearby players"; }
|
||||
};
|
||||
|
||||
// --- /y, /yell, /shout ---
|
||||
class YellCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
return sendAndSwitch(ctx, game::ChatType::YELL, 1);
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"y", "yell", "shout"}; }
|
||||
std::string helpText() const override { return "Yell to a wider area"; }
|
||||
};
|
||||
|
||||
// --- /p, /party ---
|
||||
class PartyCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
return sendAndSwitch(ctx, game::ChatType::PARTY, 2);
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"p", "party"}; }
|
||||
std::string helpText() const override { return "Party chat"; }
|
||||
};
|
||||
|
||||
// --- /g, /guild ---
|
||||
class GuildChatCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
return sendAndSwitch(ctx, game::ChatType::GUILD, 3);
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"g", "guild"}; }
|
||||
std::string helpText() const override { return "Guild chat"; }
|
||||
};
|
||||
|
||||
// --- /raid, /rsay, /ra ---
|
||||
class RaidChatCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
return sendAndSwitch(ctx, game::ChatType::RAID, 5);
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"raid", "rsay", "ra"}; }
|
||||
std::string helpText() const override { return "Raid chat"; }
|
||||
};
|
||||
|
||||
// --- /raidwarning, /rw ---
|
||||
class RaidWarningCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
return sendAndSwitch(ctx, game::ChatType::RAID_WARNING, 8);
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"raidwarning", "rw"}; }
|
||||
std::string helpText() const override { return "Raid warning"; }
|
||||
};
|
||||
|
||||
// --- /officer, /o, /osay ---
|
||||
class OfficerCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
return sendAndSwitch(ctx, game::ChatType::OFFICER, 6);
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"officer", "o", "osay"}; }
|
||||
std::string helpText() const override { return "Guild officer chat"; }
|
||||
};
|
||||
|
||||
// --- /battleground, /bg ---
|
||||
class BattlegroundChatCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
return sendAndSwitch(ctx, game::ChatType::BATTLEGROUND, 7);
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"battleground", "bg"}; }
|
||||
std::string helpText() const override { return "Battleground chat"; }
|
||||
};
|
||||
|
||||
// --- /instance, /i ---
|
||||
class InstanceChatCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
return sendAndSwitch(ctx, game::ChatType::PARTY, 9);
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"instance", "i"}; }
|
||||
std::string helpText() const override { return "Instance chat"; }
|
||||
};
|
||||
|
||||
// --- /join ---
|
||||
class JoinCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (ctx.args.empty() && ctx.gameHandler.hasPendingBgInvite()) {
|
||||
ctx.gameHandler.acceptBattlefield();
|
||||
return {};
|
||||
}
|
||||
if (!ctx.args.empty()) {
|
||||
size_t pwStart = ctx.args.find(' ');
|
||||
std::string channelName = (pwStart != std::string::npos) ? ctx.args.substr(0, pwStart) : ctx.args;
|
||||
std::string password = (pwStart != std::string::npos) ? ctx.args.substr(pwStart + 1) : "";
|
||||
ctx.gameHandler.joinChannel(channelName, password);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"join"}; }
|
||||
std::string helpText() const override { return "Join a chat channel"; }
|
||||
};
|
||||
|
||||
// --- /leave (channel) ---
|
||||
// Note: /leave without args is handled by group_commands (leave party).
|
||||
// This command only triggers with args (channel name).
|
||||
class LeaveChannelCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (!ctx.args.empty()) {
|
||||
ctx.gameHandler.leaveChannel(ctx.args);
|
||||
}
|
||||
// If no args, the group LeaveCommand will handle /leave (leave party)
|
||||
// so we return not-handled to allow fallthrough
|
||||
if (ctx.args.empty()) return {false, false};
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"leavechannel"}; }
|
||||
std::string helpText() const override { return "Leave a chat channel"; }
|
||||
};
|
||||
|
||||
// --- /wts, /wtb ---
|
||||
class TradeChannelCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (ctx.args.empty()) return {false, false};
|
||||
const std::string tag = (ctx.fullCommand == "wts") ? "[WTS] " : "[WTB] ";
|
||||
std::string tradeChan;
|
||||
for (const auto& ch : ctx.gameHandler.getJoinedChannels()) {
|
||||
std::string chLow = ch;
|
||||
for (char& c : chLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
if (chLow.rfind("trade", 0) == 0) { tradeChan = ch; break; }
|
||||
}
|
||||
if (tradeChan.empty()) {
|
||||
game::MessageChatData errMsg;
|
||||
errMsg.type = game::ChatType::SYSTEM;
|
||||
errMsg.language = game::ChatLanguage::UNIVERSAL;
|
||||
errMsg.message = "You are not in the Trade channel.";
|
||||
ctx.gameHandler.addLocalChatMessage(errMsg);
|
||||
return {};
|
||||
}
|
||||
ctx.gameHandler.sendChatMessage(game::ChatType::CHANNEL, tag + ctx.args, tradeChan);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"wts", "wtb"}; }
|
||||
std::string helpText() const override { return "Send to Trade channel ([WTS]/[WTB] prefix)"; }
|
||||
};
|
||||
|
||||
// --- /1 through /9 — channel shortcuts ---
|
||||
class ChannelNumberCommand : public IChatCommand {
|
||||
public:
|
||||
explicit ChannelNumberCommand(int num) : num_(num), alias_(std::to_string(num)) {}
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
std::string channelName = ctx.gameHandler.getChannelByIndex(num_);
|
||||
if (channelName.empty()) {
|
||||
game::MessageChatData errMsg;
|
||||
errMsg.type = game::ChatType::SYSTEM;
|
||||
errMsg.message = "You are not in channel " + std::to_string(num_) + ".";
|
||||
ctx.gameHandler.addLocalChatMessage(errMsg);
|
||||
return {};
|
||||
}
|
||||
if (!ctx.args.empty()) {
|
||||
ctx.gameHandler.sendChatMessage(game::ChatType::CHANNEL, ctx.args, channelName);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {alias_}; }
|
||||
std::string helpText() const override { return "Send to channel " + alias_; }
|
||||
private:
|
||||
int num_;
|
||||
std::string alias_;
|
||||
};
|
||||
|
||||
// --- /w, /whisper, /tell, /t ---
|
||||
class WhisperCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.panel.setSelectedChatType(4); // Switch to whisper mode
|
||||
if (!ctx.args.empty()) {
|
||||
size_t msgStart = ctx.args.find(' ');
|
||||
if (msgStart != std::string::npos) {
|
||||
// /w PlayerName message — send whisper immediately (PortBot-aware)
|
||||
std::string target = ctx.args.substr(0, msgStart);
|
||||
std::string message = ctx.args.substr(msgStart + 1);
|
||||
sendWhisperOrPortBot(ctx.gameHandler, target, message);
|
||||
// Set whisper target for future messages
|
||||
char* buf = ctx.panel.getWhisperTargetBuffer();
|
||||
size_t sz = ctx.panel.getWhisperTargetBufferSize();
|
||||
strncpy(buf, target.c_str(), sz - 1);
|
||||
buf[sz - 1] = '\0';
|
||||
} else {
|
||||
// /w PlayerName — switch to whisper mode with target set
|
||||
char* buf = ctx.panel.getWhisperTargetBuffer();
|
||||
size_t sz = ctx.panel.getWhisperTargetBufferSize();
|
||||
strncpy(buf, ctx.args.c_str(), sz - 1);
|
||||
buf[sz - 1] = '\0';
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"w", "whisper", "tell", "t"}; }
|
||||
std::string helpText() const override { return "Whisper to a player"; }
|
||||
};
|
||||
|
||||
// --- /r, /reply ---
|
||||
class ReplyCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.panel.setSelectedChatType(4);
|
||||
std::string lastSender = ctx.gameHandler.getLastWhisperSender();
|
||||
if (lastSender.empty()) {
|
||||
game::MessageChatData sysMsg;
|
||||
sysMsg.type = game::ChatType::SYSTEM;
|
||||
sysMsg.language = game::ChatLanguage::UNIVERSAL;
|
||||
sysMsg.message = "No one has whispered you yet.";
|
||||
ctx.gameHandler.addLocalChatMessage(sysMsg);
|
||||
return {};
|
||||
}
|
||||
char* buf = ctx.panel.getWhisperTargetBuffer();
|
||||
size_t sz = ctx.panel.getWhisperTargetBufferSize();
|
||||
strncpy(buf, lastSender.c_str(), sz - 1);
|
||||
buf[sz - 1] = '\0';
|
||||
if (!ctx.args.empty()) {
|
||||
// PortBot-aware whisper send
|
||||
sendWhisperOrPortBot(ctx.gameHandler, lastSender, ctx.args);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"r", "reply"}; }
|
||||
std::string helpText() const override { return "Reply to last whisper"; }
|
||||
};
|
||||
|
||||
// --- Registration ---
|
||||
void registerChannelCommands(ChatCommandRegistry& reg) {
|
||||
reg.registerCommand(std::make_unique<SayCommand>());
|
||||
reg.registerCommand(std::make_unique<YellCommand>());
|
||||
reg.registerCommand(std::make_unique<PartyCommand>());
|
||||
reg.registerCommand(std::make_unique<GuildChatCommand>());
|
||||
reg.registerCommand(std::make_unique<RaidChatCommand>());
|
||||
reg.registerCommand(std::make_unique<RaidWarningCommand>());
|
||||
reg.registerCommand(std::make_unique<OfficerCommand>());
|
||||
reg.registerCommand(std::make_unique<BattlegroundChatCommand>());
|
||||
reg.registerCommand(std::make_unique<InstanceChatCommand>());
|
||||
reg.registerCommand(std::make_unique<JoinCommand>());
|
||||
reg.registerCommand(std::make_unique<LeaveChannelCommand>());
|
||||
reg.registerCommand(std::make_unique<TradeChannelCommand>());
|
||||
for (int n = 1; n <= 9; ++n)
|
||||
reg.registerCommand(std::make_unique<ChannelNumberCommand>(n));
|
||||
reg.registerCommand(std::make_unique<WhisperCommand>());
|
||||
reg.registerCommand(std::make_unique<ReplyCommand>());
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
539
src/ui/chat/commands/combat_commands.cpp
Normal file
539
src/ui/chat/commands/combat_commands.cpp
Normal 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
|
||||
230
src/ui/chat/commands/emote_commands.cpp
Normal file
230
src/ui/chat/commands/emote_commands.cpp
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
// Emote/stance commands: /sit, /stand, /kneel, /dismount, /cancelform,
|
||||
// /cancelaura, /cancellogout, /logout, /camp, /quit, /exit,
|
||||
// pet commands (/petattack, /petfollow, etc.)
|
||||
// Moved from ChatPanel::sendChatMessage() if/else chain (Phase 3).
|
||||
#include "ui/chat/i_chat_command.hpp"
|
||||
#include "ui/chat_panel.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "rendering/animation_controller.hpp"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
// --- /sit ---
|
||||
class SitCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.setStandState(1);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"sit"}; }
|
||||
std::string helpText() const override { return "Sit down"; }
|
||||
};
|
||||
|
||||
// --- /stand ---
|
||||
class StandCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.setStandState(0);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"stand"}; }
|
||||
std::string helpText() const override { return "Stand up"; }
|
||||
};
|
||||
|
||||
// --- /kneel ---
|
||||
class KneelCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.setStandState(8);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"kneel"}; }
|
||||
std::string helpText() const override { return "Kneel"; }
|
||||
};
|
||||
|
||||
// --- /logout, /camp, /quit, /exit ---
|
||||
class LogoutEmoteCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.requestLogout();
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"camp", "quit", "exit"}; }
|
||||
std::string helpText() const override { return "Logout / quit game"; }
|
||||
};
|
||||
|
||||
// --- /cancellogout ---
|
||||
class CancelLogoutCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.cancelLogout();
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"cancellogout"}; }
|
||||
std::string helpText() const override { return "Cancel pending logout"; }
|
||||
};
|
||||
|
||||
// --- /dismount ---
|
||||
class DismountCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.dismount();
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"dismount"}; }
|
||||
std::string helpText() const override { return "Dismount"; }
|
||||
};
|
||||
|
||||
// --- /cancelform, /cancelshapeshift ---
|
||||
class CancelFormCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
for (const auto& aura : ctx.gameHandler.getPlayerAuras()) {
|
||||
if (aura.spellId == 0) continue;
|
||||
if (aura.flags & 0x20) {
|
||||
ctx.gameHandler.cancelAura(aura.spellId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"cancelform", "cancelshapeshift"}; }
|
||||
std::string helpText() const override { return "Cancel shapeshift form"; }
|
||||
};
|
||||
|
||||
// --- /cancelaura ---
|
||||
class CancelAuraCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (ctx.args.empty()) return {false, false};
|
||||
std::string auraArg = ctx.args;
|
||||
while (!auraArg.empty() && auraArg.front() == ' ') auraArg.erase(auraArg.begin());
|
||||
while (!auraArg.empty() && auraArg.back() == ' ') auraArg.pop_back();
|
||||
// Try numeric ID first
|
||||
{
|
||||
std::string numStr = auraArg;
|
||||
if (!numStr.empty() && numStr.front() == '#') numStr.erase(numStr.begin());
|
||||
bool isNum = !numStr.empty() &&
|
||||
std::all_of(numStr.begin(), numStr.end(),
|
||||
[](unsigned char c){ return std::isdigit(c); });
|
||||
if (isNum) {
|
||||
uint32_t spellId = 0;
|
||||
try { spellId = static_cast<uint32_t>(std::stoul(numStr)); } catch (...) {}
|
||||
if (spellId) ctx.gameHandler.cancelAura(spellId);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
// Name match against player auras
|
||||
std::string argLow = auraArg;
|
||||
for (char& c : argLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
for (const auto& aura : ctx.gameHandler.getPlayerAuras()) {
|
||||
if (aura.spellId == 0) continue;
|
||||
std::string sn = ctx.gameHandler.getSpellName(aura.spellId);
|
||||
for (char& c : sn) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
if (sn == argLow) {
|
||||
ctx.gameHandler.cancelAura(aura.spellId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"cancelaura"}; }
|
||||
std::string helpText() const override { return "Cancel a specific aura/buff"; }
|
||||
};
|
||||
|
||||
// --- Pet commands ---
|
||||
class PetAttackCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
uint64_t target = ctx.gameHandler.hasTarget() ? ctx.gameHandler.getTargetGuid() : 0;
|
||||
ctx.gameHandler.sendPetAction(5, target);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"petattack"}; }
|
||||
std::string helpText() const override { return "Pet: attack target"; }
|
||||
};
|
||||
|
||||
class PetFollowCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.sendPetAction(2, 0);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"petfollow"}; }
|
||||
std::string helpText() const override { return "Pet: follow owner"; }
|
||||
};
|
||||
|
||||
class PetStayCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.sendPetAction(3, 0);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"petstay", "pethalt"}; }
|
||||
std::string helpText() const override { return "Pet: stay"; }
|
||||
};
|
||||
|
||||
class PetPassiveCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.sendPetAction(1, 0);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"petpassive"}; }
|
||||
std::string helpText() const override { return "Pet: passive mode"; }
|
||||
};
|
||||
|
||||
class PetDefensiveCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.sendPetAction(4, 0);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"petdefensive"}; }
|
||||
std::string helpText() const override { return "Pet: defensive mode"; }
|
||||
};
|
||||
|
||||
class PetAggressiveCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.sendPetAction(6, 0);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"petaggressive"}; }
|
||||
std::string helpText() const override { return "Pet: aggressive mode"; }
|
||||
};
|
||||
|
||||
class PetDismissCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.dismissPet();
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"petdismiss"}; }
|
||||
std::string helpText() const override { return "Dismiss pet"; }
|
||||
};
|
||||
|
||||
// --- Registration ---
|
||||
void registerEmoteCommands(ChatCommandRegistry& reg) {
|
||||
reg.registerCommand(std::make_unique<SitCommand>());
|
||||
reg.registerCommand(std::make_unique<StandCommand>());
|
||||
reg.registerCommand(std::make_unique<KneelCommand>());
|
||||
reg.registerCommand(std::make_unique<LogoutEmoteCommand>());
|
||||
reg.registerCommand(std::make_unique<CancelLogoutCommand>());
|
||||
reg.registerCommand(std::make_unique<DismountCommand>());
|
||||
reg.registerCommand(std::make_unique<CancelFormCommand>());
|
||||
reg.registerCommand(std::make_unique<CancelAuraCommand>());
|
||||
reg.registerCommand(std::make_unique<PetAttackCommand>());
|
||||
reg.registerCommand(std::make_unique<PetFollowCommand>());
|
||||
reg.registerCommand(std::make_unique<PetStayCommand>());
|
||||
reg.registerCommand(std::make_unique<PetPassiveCommand>());
|
||||
reg.registerCommand(std::make_unique<PetDefensiveCommand>());
|
||||
reg.registerCommand(std::make_unique<PetAggressiveCommand>());
|
||||
reg.registerCommand(std::make_unique<PetDismissCommand>());
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
127
src/ui/chat/commands/gm_commands.cpp
Normal file
127
src/ui/chat/commands/gm_commands.cpp
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
// GM commands: /gmhelp, /gmcommands — local help for server-side dot-prefix commands.
|
||||
// Also provides the gm_commands::getCompletions() function used by tab-completion.
|
||||
// The actual GM commands (.gm, .tele, etc.) are sent to the server as SAY messages;
|
||||
// the server (AzerothCore) does the real work. This file just adds discoverability.
|
||||
#include "ui/chat/i_chat_command.hpp"
|
||||
#include "ui/chat/chat_command_registry.hpp"
|
||||
#include "ui/chat/gm_command_data.hpp"
|
||||
#include "ui/chat/chat_utils.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// gm_commands namespace — GM command lookup helpers used by tab-completion
|
||||
// and the /gmhelp command.
|
||||
// ---------------------------------------------------------------------------
|
||||
namespace gm_commands {
|
||||
|
||||
std::vector<std::string> getCompletions(const std::string& prefix) {
|
||||
std::vector<std::string> results;
|
||||
for (const auto& cmd : kGmCommands) {
|
||||
std::string dotName = "." + std::string(cmd.name);
|
||||
if (dotName.size() >= prefix.size() &&
|
||||
dotName.compare(0, prefix.size(), prefix) == 0) {
|
||||
results.push_back(dotName);
|
||||
}
|
||||
}
|
||||
std::sort(results.begin(), results.end());
|
||||
return results;
|
||||
}
|
||||
|
||||
const GmCommandEntry* find(const std::string& name) {
|
||||
for (const auto& cmd : kGmCommands) {
|
||||
if (cmd.name == name) return &cmd;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace gm_commands
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// /gmhelp [filter] — display GM command reference locally.
|
||||
// ---------------------------------------------------------------------------
|
||||
class GmHelpCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
std::string filter = ctx.args;
|
||||
for (char& c : filter) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
|
||||
// Trim leading/trailing whitespace
|
||||
while (!filter.empty() && std::isspace(static_cast<unsigned char>(filter.front()))) filter.erase(filter.begin());
|
||||
while (!filter.empty() && std::isspace(static_cast<unsigned char>(filter.back()))) filter.pop_back();
|
||||
|
||||
// If filter matches a specific command name, show detailed help
|
||||
if (!filter.empty()) {
|
||||
// Strip leading dot if user typed /gmhelp .gm
|
||||
if (filter.front() == '.') filter = filter.substr(1);
|
||||
|
||||
bool found = false;
|
||||
for (const auto& cmd : kGmCommands) {
|
||||
std::string name(cmd.name);
|
||||
if (name == filter || name.compare(0, filter.size(), filter) == 0) {
|
||||
std::string line = std::string(cmd.syntax) + " — " + std::string(cmd.help)
|
||||
+ " [sec:" + std::to_string(cmd.security) + "]";
|
||||
ctx.gameHandler.addLocalChatMessage(chat_utils::makeSystemMessage(line));
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
ctx.gameHandler.addLocalChatMessage(
|
||||
chat_utils::makeSystemMessage("No GM commands matching '" + filter + "'."));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// No filter — print category overview
|
||||
ctx.gameHandler.addLocalChatMessage(
|
||||
chat_utils::makeSystemMessage("--- GM Commands (dot-prefix, sent to server) ---"));
|
||||
ctx.gameHandler.addLocalChatMessage(
|
||||
chat_utils::makeSystemMessage("GM Mode: .gm on/off .gm fly .gm visible"));
|
||||
ctx.gameHandler.addLocalChatMessage(
|
||||
chat_utils::makeSystemMessage("Teleport: .tele <loc> .go xyz .appear .summon"));
|
||||
ctx.gameHandler.addLocalChatMessage(
|
||||
chat_utils::makeSystemMessage("Character: .levelup .additem .learn .maxskill .pinfo"));
|
||||
ctx.gameHandler.addLocalChatMessage(
|
||||
chat_utils::makeSystemMessage("Combat: .revive .die .damage .freeze .respawn"));
|
||||
ctx.gameHandler.addLocalChatMessage(
|
||||
chat_utils::makeSystemMessage("Modify: .modify money/hp/mana/speed .morph .modify scale"));
|
||||
ctx.gameHandler.addLocalChatMessage(
|
||||
chat_utils::makeSystemMessage("Cheats: .cheat god/casttime/cooldown/power/taxi/explore"));
|
||||
ctx.gameHandler.addLocalChatMessage(
|
||||
chat_utils::makeSystemMessage("Spells: .cast .aura .unaura .cooldown .setskill"));
|
||||
ctx.gameHandler.addLocalChatMessage(
|
||||
chat_utils::makeSystemMessage("Quests: .quest add/complete/remove/reward/status"));
|
||||
ctx.gameHandler.addLocalChatMessage(
|
||||
chat_utils::makeSystemMessage("NPC: .npc add/delete/info/near/say/move"));
|
||||
ctx.gameHandler.addLocalChatMessage(
|
||||
chat_utils::makeSystemMessage("Objects: .gobject add/delete/info/near/target"));
|
||||
ctx.gameHandler.addLocalChatMessage(
|
||||
chat_utils::makeSystemMessage("Lookup: .lookup item/spell/creature/quest/area/teleport"));
|
||||
ctx.gameHandler.addLocalChatMessage(
|
||||
chat_utils::makeSystemMessage("Admin: .ban .kick .mute .announce .reload"));
|
||||
ctx.gameHandler.addLocalChatMessage(
|
||||
chat_utils::makeSystemMessage("Server: .server info .server motd .save .commands .help"));
|
||||
ctx.gameHandler.addLocalChatMessage(
|
||||
chat_utils::makeSystemMessage("Use /gmhelp <command> for details (e.g. /gmhelp tele)."));
|
||||
ctx.gameHandler.addLocalChatMessage(
|
||||
chat_utils::makeSystemMessage("Tab-complete works with dot-prefix (type .te<Tab>)."));
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"gmhelp", "gmcommands"}; }
|
||||
std::string helpText() const override { return "List GM dot-commands (server-side)"; }
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Registration
|
||||
// ---------------------------------------------------------------------------
|
||||
void registerGmCommands(ChatCommandRegistry& reg) {
|
||||
reg.registerCommand(std::make_unique<GmHelpCommand>());
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
395
src/ui/chat/commands/group_commands.cpp
Normal file
395
src/ui/chat/commands/group_commands.cpp
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
// Group commands: /readycheck, /ready, /notready, /yield, /afk, /dnd,
|
||||
// /uninvite, /leave, /maintank, /mainassist, /clearmaintank,
|
||||
// /clearmainassist, /raidinfo, /raidconvert, /lootmethod,
|
||||
// /lootthreshold, /mark, /roll
|
||||
// Moved from ChatPanel::sendChatMessage() if/else chain (Phase 3).
|
||||
#include "ui/chat/i_chat_command.hpp"
|
||||
#include "ui/chat_panel.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
// --- /readycheck, /rc ---
|
||||
class ReadyCheckCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.initiateReadyCheck();
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"readycheck", "rc"}; }
|
||||
std::string helpText() const override { return "Initiate ready check"; }
|
||||
};
|
||||
|
||||
// --- /ready ---
|
||||
class ReadyCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.respondToReadyCheck(true);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"ready"}; }
|
||||
std::string helpText() const override { return "Respond yes to ready check"; }
|
||||
};
|
||||
|
||||
// --- /notready, /nr ---
|
||||
class NotReadyCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.respondToReadyCheck(false);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"notready", "nr"}; }
|
||||
std::string helpText() const override { return "Respond no to ready check"; }
|
||||
};
|
||||
|
||||
// --- /yield, /forfeit, /surrender ---
|
||||
class YieldCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.forfeitDuel();
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"yield", "forfeit", "surrender"}; }
|
||||
std::string helpText() const override { return "Forfeit current duel"; }
|
||||
};
|
||||
|
||||
// --- /afk, /away ---
|
||||
class AfkCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.toggleAfk(ctx.args);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"afk", "away"}; }
|
||||
std::string helpText() const override { return "Toggle AFK status"; }
|
||||
};
|
||||
|
||||
// --- /dnd, /busy ---
|
||||
class DndCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.toggleDnd(ctx.args);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"dnd", "busy"}; }
|
||||
std::string helpText() const override { return "Toggle Do Not Disturb"; }
|
||||
};
|
||||
|
||||
// --- /uninvite, /kick ---
|
||||
class UninviteCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (!ctx.args.empty()) {
|
||||
ctx.gameHandler.uninvitePlayer(ctx.args);
|
||||
} else {
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Usage: /uninvite <player name>";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"uninvite", "kick"}; }
|
||||
std::string helpText() const override { return "Remove player from group"; }
|
||||
};
|
||||
|
||||
// --- /leave, /leaveparty ---
|
||||
// /leave — leave party (no args) or leave channel (with args, WoW-style overload)
|
||||
class LeavePartyCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (!ctx.args.empty()) {
|
||||
// /leave ChannelName — leave a chat channel
|
||||
ctx.gameHandler.leaveChannel(ctx.args);
|
||||
} else {
|
||||
ctx.gameHandler.leaveParty();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"leave", "leaveparty"}; }
|
||||
std::string helpText() const override { return "Leave party/raid or channel"; }
|
||||
};
|
||||
|
||||
// --- /maintank, /mt ---
|
||||
class MainTankCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (ctx.gameHandler.hasTarget()) {
|
||||
ctx.gameHandler.setMainTank(ctx.gameHandler.getTargetGuid());
|
||||
} else {
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "You must target a player to set as main tank.";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"maintank", "mt"}; }
|
||||
std::string helpText() const override { return "Set target as main tank"; }
|
||||
};
|
||||
|
||||
// --- /mainassist, /ma ---
|
||||
class MainAssistCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (ctx.gameHandler.hasTarget()) {
|
||||
ctx.gameHandler.setMainAssist(ctx.gameHandler.getTargetGuid());
|
||||
} else {
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "You must target a player to set as main assist.";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"mainassist", "ma"}; }
|
||||
std::string helpText() const override { return "Set target as main assist"; }
|
||||
};
|
||||
|
||||
// --- /clearmaintank ---
|
||||
class ClearMainTankCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.clearMainTank();
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"clearmaintank"}; }
|
||||
std::string helpText() const override { return "Clear main tank assignment"; }
|
||||
};
|
||||
|
||||
// --- /clearmainassist ---
|
||||
class ClearMainAssistCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.clearMainAssist();
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"clearmainassist"}; }
|
||||
std::string helpText() const override { return "Clear main assist assignment"; }
|
||||
};
|
||||
|
||||
// --- /raidinfo ---
|
||||
class RaidInfoCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.requestRaidInfo();
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"raidinfo"}; }
|
||||
std::string helpText() const override { return "Show raid instance lockouts"; }
|
||||
};
|
||||
|
||||
// --- /raidconvert ---
|
||||
class RaidConvertCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.convertToRaid();
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"raidconvert"}; }
|
||||
std::string helpText() const override { return "Convert party to raid"; }
|
||||
};
|
||||
|
||||
// --- /lootmethod, /grouploot, /setloot ---
|
||||
class LootMethodCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (!ctx.gameHandler.isInGroup()) {
|
||||
ctx.gameHandler.addUIError("You are not in a group.");
|
||||
return {};
|
||||
}
|
||||
if (ctx.args.empty()) {
|
||||
static constexpr const char* kMethodNames[] = {
|
||||
"Free for All", "Round Robin", "Master Looter", "Group Loot", "Need Before Greed"
|
||||
};
|
||||
const auto& pd = ctx.gameHandler.getPartyData();
|
||||
const char* cur = (pd.lootMethod < 5) ? kMethodNames[pd.lootMethod] : "Unknown";
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = std::string("Current loot method: ") + cur;
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
msg.message = "Usage: /lootmethod ffa|roundrobin|master|group|needbeforegreed";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
return {};
|
||||
}
|
||||
std::string arg = ctx.args;
|
||||
for (auto& c : arg) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
uint32_t method = 0xFFFFFFFF;
|
||||
if (arg == "ffa" || arg == "freeforall") method = 0;
|
||||
else if (arg == "roundrobin" || arg == "rr") method = 1;
|
||||
else if (arg == "master" || arg == "masterloot") method = 2;
|
||||
else if (arg == "group" || arg == "grouploot") method = 3;
|
||||
else if (arg == "needbeforegreed" || arg == "nbg" || arg == "need") method = 4;
|
||||
|
||||
if (method == 0xFFFFFFFF) {
|
||||
ctx.gameHandler.addUIError("Unknown loot method. Use: ffa, roundrobin, master, group, needbeforegreed");
|
||||
} else {
|
||||
const auto& pd = ctx.gameHandler.getPartyData();
|
||||
uint64_t masterGuid = (method == 2) ? ctx.gameHandler.getPlayerGuid() : 0;
|
||||
ctx.gameHandler.sendSetLootMethod(method, pd.lootThreshold, masterGuid);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"lootmethod", "grouploot", "setloot"}; }
|
||||
std::string helpText() const override { return "Set loot method"; }
|
||||
};
|
||||
|
||||
// --- /lootthreshold ---
|
||||
class LootThresholdCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (!ctx.gameHandler.isInGroup()) {
|
||||
ctx.gameHandler.addUIError("You are not in a group.");
|
||||
return {};
|
||||
}
|
||||
if (ctx.args.empty()) {
|
||||
const auto& pd = ctx.gameHandler.getPartyData();
|
||||
static constexpr const char* kQualityNames[] = {
|
||||
"Poor (grey)", "Common (white)", "Uncommon (green)",
|
||||
"Rare (blue)", "Epic (purple)", "Legendary (orange)"
|
||||
};
|
||||
const char* cur = (pd.lootThreshold < 6) ? kQualityNames[pd.lootThreshold] : "Unknown";
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = std::string("Current loot threshold: ") + cur;
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
msg.message = "Usage: /lootthreshold <0-5> (0=Poor, 1=Common, 2=Uncommon, 3=Rare, 4=Epic, 5=Legendary)";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
return {};
|
||||
}
|
||||
std::string arg = ctx.args;
|
||||
while (!arg.empty() && arg.front() == ' ') arg.erase(arg.begin());
|
||||
uint32_t threshold = 0xFFFFFFFF;
|
||||
if (arg.size() == 1 && arg[0] >= '0' && arg[0] <= '5') {
|
||||
threshold = static_cast<uint32_t>(arg[0] - '0');
|
||||
} else {
|
||||
for (auto& c : arg) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
if (arg == "poor" || arg == "grey" || arg == "gray") threshold = 0;
|
||||
else if (arg == "common" || arg == "white") threshold = 1;
|
||||
else if (arg == "uncommon" || arg == "green") threshold = 2;
|
||||
else if (arg == "rare" || arg == "blue") threshold = 3;
|
||||
else if (arg == "epic" || arg == "purple") threshold = 4;
|
||||
else if (arg == "legendary" || arg == "orange") threshold = 5;
|
||||
}
|
||||
if (threshold == 0xFFFFFFFF) {
|
||||
ctx.gameHandler.addUIError("Invalid threshold. Use 0-5 or: poor, common, uncommon, rare, epic, legendary");
|
||||
} else {
|
||||
const auto& pd = ctx.gameHandler.getPartyData();
|
||||
uint64_t masterGuid = (pd.lootMethod == 2) ? ctx.gameHandler.getPlayerGuid() : 0;
|
||||
ctx.gameHandler.sendSetLootMethod(pd.lootMethod, threshold, masterGuid);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"lootthreshold"}; }
|
||||
std::string helpText() const override { return "Set loot quality threshold"; }
|
||||
};
|
||||
|
||||
// --- /mark, /marktarget, /raidtarget ---
|
||||
class MarkCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (!ctx.gameHandler.hasTarget()) {
|
||||
game::MessageChatData noTgt;
|
||||
noTgt.type = game::ChatType::SYSTEM;
|
||||
noTgt.language = game::ChatLanguage::UNIVERSAL;
|
||||
noTgt.message = "No target selected.";
|
||||
ctx.gameHandler.addLocalChatMessage(noTgt);
|
||||
return {};
|
||||
}
|
||||
static constexpr const char* kMarkWords[] = {
|
||||
"star", "circle", "diamond", "triangle", "moon", "square", "cross", "skull"
|
||||
};
|
||||
uint8_t icon = 7; // default: skull
|
||||
if (!ctx.args.empty()) {
|
||||
std::string argLow = ctx.args;
|
||||
for (auto& c : argLow) c = static_cast<char>(std::tolower(c));
|
||||
while (!argLow.empty() && argLow.front() == ' ') argLow.erase(argLow.begin());
|
||||
if (argLow == "clear" || argLow == "0" || argLow == "none") {
|
||||
ctx.gameHandler.setRaidMark(ctx.gameHandler.getTargetGuid(), 0xFF);
|
||||
return {};
|
||||
}
|
||||
bool found = false;
|
||||
for (int mi = 0; mi < 8; ++mi) {
|
||||
if (argLow == kMarkWords[mi]) { icon = static_cast<uint8_t>(mi); found = true; break; }
|
||||
}
|
||||
if (!found && !argLow.empty() && argLow[0] >= '1' && argLow[0] <= '8') {
|
||||
icon = static_cast<uint8_t>(argLow[0] - '1');
|
||||
found = true;
|
||||
}
|
||||
if (!found) {
|
||||
game::MessageChatData badArg;
|
||||
badArg.type = game::ChatType::SYSTEM;
|
||||
badArg.language = game::ChatLanguage::UNIVERSAL;
|
||||
badArg.message = "Unknown mark. Use: star circle diamond triangle moon square cross skull";
|
||||
ctx.gameHandler.addLocalChatMessage(badArg);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
ctx.gameHandler.setRaidMark(ctx.gameHandler.getTargetGuid(), icon);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"mark", "marktarget", "raidtarget"}; }
|
||||
std::string helpText() const override { return "Set raid target mark on target"; }
|
||||
};
|
||||
|
||||
// --- /roll, /random, /rnd ---
|
||||
class RollCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
uint32_t minRoll = 1;
|
||||
uint32_t maxRoll = 100;
|
||||
if (!ctx.args.empty()) {
|
||||
size_t dashPos = ctx.args.find('-');
|
||||
size_t spacePos = ctx.args.find(' ');
|
||||
if (dashPos != std::string::npos) {
|
||||
try {
|
||||
minRoll = std::stoul(ctx.args.substr(0, dashPos));
|
||||
maxRoll = std::stoul(ctx.args.substr(dashPos + 1));
|
||||
} catch (...) {}
|
||||
} else if (spacePos != std::string::npos) {
|
||||
try {
|
||||
minRoll = std::stoul(ctx.args.substr(0, spacePos));
|
||||
maxRoll = std::stoul(ctx.args.substr(spacePos + 1));
|
||||
} catch (...) {}
|
||||
} else {
|
||||
try { maxRoll = std::stoul(ctx.args); } catch (...) {}
|
||||
}
|
||||
}
|
||||
ctx.gameHandler.randomRoll(minRoll, maxRoll);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"roll", "random", "rnd"}; }
|
||||
std::string helpText() const override { return "Random dice roll"; }
|
||||
};
|
||||
|
||||
// --- Registration ---
|
||||
void registerGroupCommands(ChatCommandRegistry& reg) {
|
||||
reg.registerCommand(std::make_unique<ReadyCheckCommand>());
|
||||
reg.registerCommand(std::make_unique<ReadyCommand>());
|
||||
reg.registerCommand(std::make_unique<NotReadyCommand>());
|
||||
reg.registerCommand(std::make_unique<YieldCommand>());
|
||||
reg.registerCommand(std::make_unique<AfkCommand>());
|
||||
reg.registerCommand(std::make_unique<DndCommand>());
|
||||
reg.registerCommand(std::make_unique<UninviteCommand>());
|
||||
reg.registerCommand(std::make_unique<LeavePartyCommand>());
|
||||
reg.registerCommand(std::make_unique<MainTankCommand>());
|
||||
reg.registerCommand(std::make_unique<MainAssistCommand>());
|
||||
reg.registerCommand(std::make_unique<ClearMainTankCommand>());
|
||||
reg.registerCommand(std::make_unique<ClearMainAssistCommand>());
|
||||
reg.registerCommand(std::make_unique<RaidInfoCommand>());
|
||||
reg.registerCommand(std::make_unique<RaidConvertCommand>());
|
||||
reg.registerCommand(std::make_unique<LootMethodCommand>());
|
||||
reg.registerCommand(std::make_unique<LootThresholdCommand>());
|
||||
reg.registerCommand(std::make_unique<MarkCommand>());
|
||||
reg.registerCommand(std::make_unique<RollCommand>());
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
203
src/ui/chat/commands/guild_commands.cpp
Normal file
203
src/ui/chat/commands/guild_commands.cpp
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
// Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit,
|
||||
// /ginvite, /gkick, /gcreate, /gdisband, /gleader
|
||||
// Moved from ChatPanel::sendChatMessage() if/else chain (Phase 3).
|
||||
#include "ui/chat/i_chat_command.hpp"
|
||||
#include "ui/chat_panel.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
// --- /ginfo, /guildinfo ---
|
||||
class GuildInfoCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.requestGuildInfo();
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"ginfo", "guildinfo"}; }
|
||||
std::string helpText() const override { return "Show guild info"; }
|
||||
};
|
||||
|
||||
// --- /groster, /guildroster ---
|
||||
class GuildRosterCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.requestGuildRoster();
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"groster", "guildroster"}; }
|
||||
std::string helpText() const override { return "Show guild roster"; }
|
||||
};
|
||||
|
||||
// --- /gmotd, /guildmotd ---
|
||||
class GuildMotdCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (!ctx.args.empty()) {
|
||||
ctx.gameHandler.setGuildMotd(ctx.args);
|
||||
return {};
|
||||
}
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Usage: /gmotd <message>";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"gmotd", "guildmotd"}; }
|
||||
std::string helpText() const override { return "Set guild message of the day"; }
|
||||
};
|
||||
|
||||
// --- /gpromote, /guildpromote ---
|
||||
class GuildPromoteCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (!ctx.args.empty()) {
|
||||
ctx.gameHandler.promoteGuildMember(ctx.args);
|
||||
return {};
|
||||
}
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Usage: /gpromote <player>";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"gpromote", "guildpromote"}; }
|
||||
std::string helpText() const override { return "Promote guild member"; }
|
||||
};
|
||||
|
||||
// --- /gdemote, /guilddemote ---
|
||||
class GuildDemoteCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (!ctx.args.empty()) {
|
||||
ctx.gameHandler.demoteGuildMember(ctx.args);
|
||||
return {};
|
||||
}
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Usage: /gdemote <player>";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"gdemote", "guilddemote"}; }
|
||||
std::string helpText() const override { return "Demote guild member"; }
|
||||
};
|
||||
|
||||
// --- /gquit, /guildquit, /leaveguild ---
|
||||
class GuildQuitCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.leaveGuild();
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"gquit", "guildquit", "leaveguild"}; }
|
||||
std::string helpText() const override { return "Leave guild"; }
|
||||
};
|
||||
|
||||
// --- /ginvite, /guildinvite ---
|
||||
class GuildInviteCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (!ctx.args.empty()) {
|
||||
ctx.gameHandler.inviteToGuild(ctx.args);
|
||||
return {};
|
||||
}
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Usage: /ginvite <player>";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"ginvite", "guildinvite"}; }
|
||||
std::string helpText() const override { return "Invite player to guild"; }
|
||||
};
|
||||
|
||||
// --- /gkick, /guildkick ---
|
||||
class GuildKickCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (!ctx.args.empty()) {
|
||||
ctx.gameHandler.kickGuildMember(ctx.args);
|
||||
return {};
|
||||
}
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Usage: /gkick <player>";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"gkick", "guildkick"}; }
|
||||
std::string helpText() const override { return "Kick player from guild"; }
|
||||
};
|
||||
|
||||
// --- /gcreate, /guildcreate ---
|
||||
class GuildCreateCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (!ctx.args.empty()) {
|
||||
ctx.gameHandler.createGuild(ctx.args);
|
||||
return {};
|
||||
}
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Usage: /gcreate <guild name>";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"gcreate", "guildcreate"}; }
|
||||
std::string helpText() const override { return "Create a guild"; }
|
||||
};
|
||||
|
||||
// --- /gdisband, /guilddisband ---
|
||||
class GuildDisbandCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.disbandGuild();
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"gdisband", "guilddisband"}; }
|
||||
std::string helpText() const override { return "Disband guild"; }
|
||||
};
|
||||
|
||||
// --- /gleader, /guildleader ---
|
||||
class GuildLeaderCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (!ctx.args.empty()) {
|
||||
ctx.gameHandler.setGuildLeader(ctx.args);
|
||||
return {};
|
||||
}
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Usage: /gleader <player>";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"gleader", "guildleader"}; }
|
||||
std::string helpText() const override { return "Transfer guild leadership"; }
|
||||
};
|
||||
|
||||
// --- Registration ---
|
||||
void registerGuildCommands(ChatCommandRegistry& reg) {
|
||||
reg.registerCommand(std::make_unique<GuildInfoCommand>());
|
||||
reg.registerCommand(std::make_unique<GuildRosterCommand>());
|
||||
reg.registerCommand(std::make_unique<GuildMotdCommand>());
|
||||
reg.registerCommand(std::make_unique<GuildPromoteCommand>());
|
||||
reg.registerCommand(std::make_unique<GuildDemoteCommand>());
|
||||
reg.registerCommand(std::make_unique<GuildQuitCommand>());
|
||||
reg.registerCommand(std::make_unique<GuildInviteCommand>());
|
||||
reg.registerCommand(std::make_unique<GuildKickCommand>());
|
||||
reg.registerCommand(std::make_unique<GuildCreateCommand>());
|
||||
reg.registerCommand(std::make_unique<GuildDisbandCommand>());
|
||||
reg.registerCommand(std::make_unique<GuildLeaderCommand>());
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
124
src/ui/chat/commands/help_commands.cpp
Normal file
124
src/ui/chat/commands/help_commands.cpp
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
// Help commands: /help, /chathelp, /macrohelp
|
||||
// Moved from ChatPanel::sendChatMessage() if/else chain (Phase 3).
|
||||
#include "ui/chat/i_chat_command.hpp"
|
||||
#include "ui/chat_panel.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
// --- /help, /? ---
|
||||
class HelpCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
static constexpr const char* kHelpLines[] = {
|
||||
"--- Wowee Slash Commands ---",
|
||||
"Chat: /s /y /p /g /raid /rw /o /bg /w <name> /r /join /leave",
|
||||
"Social: /who /friend add/remove /ignore /unignore",
|
||||
"Party: /invite /uninvite /leave /readycheck /mark /roll",
|
||||
" /maintank /mainassist /raidconvert /raidinfo",
|
||||
" /lootmethod /lootthreshold",
|
||||
"Guild: /ginvite /gkick /gquit /gpromote /gdemote /gmotd",
|
||||
" /gleader /groster /ginfo /gcreate /gdisband",
|
||||
"Combat: /cast /castsequence /use /startattack /stopattack",
|
||||
" /stopcasting /duel /forfeit /pvp /assist",
|
||||
" /follow /stopfollow /threat /combatlog",
|
||||
"Items: /use <item> /equip <item> /equipset [name]",
|
||||
"Target: /target /cleartarget /focus /clearfocus /inspect",
|
||||
"Movement: /sit /stand /kneel /dismount",
|
||||
"Misc: /played /time /zone /loc /afk /dnd /helm /cloak",
|
||||
" /trade /score /unstuck /logout /quit /exit /ticket",
|
||||
" /screenshot /difficulty",
|
||||
" /macrohelp /chathelp /help /gmhelp",
|
||||
"GM: .command (dot-prefix sent to server, /gmhelp for list)",
|
||||
};
|
||||
for (const char* line : kHelpLines) {
|
||||
game::MessageChatData helpMsg;
|
||||
helpMsg.type = game::ChatType::SYSTEM;
|
||||
helpMsg.language = game::ChatLanguage::UNIVERSAL;
|
||||
helpMsg.message = line;
|
||||
ctx.gameHandler.addLocalChatMessage(helpMsg);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"help", "?"}; }
|
||||
std::string helpText() const override { return "List all slash commands"; }
|
||||
};
|
||||
|
||||
// --- /chathelp ---
|
||||
class ChatHelpCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
static constexpr const char* kChatHelp[] = {
|
||||
"--- Chat Channel Commands ---",
|
||||
"/s [msg] Say to nearby players",
|
||||
"/y [msg] Yell to a wider area",
|
||||
"/w <name> [msg] Whisper to player",
|
||||
"/r [msg] Reply to last whisper",
|
||||
"/p [msg] Party chat",
|
||||
"/g [msg] Guild chat",
|
||||
"/o [msg] Guild officer chat",
|
||||
"/raid [msg] Raid chat",
|
||||
"/rw [msg] Raid warning",
|
||||
"/bg [msg] Battleground chat",
|
||||
"/1 [msg] General channel",
|
||||
"/2 [msg] Trade channel (also /wts /wtb)",
|
||||
"/<N> [msg] Channel by number",
|
||||
"/join <chan> Join a channel",
|
||||
"/leave <chan> Leave a channel",
|
||||
"/afk [msg] Set AFK status",
|
||||
"/dnd [msg] Set Do Not Disturb",
|
||||
};
|
||||
for (const char* line : kChatHelp) {
|
||||
game::MessageChatData helpMsg;
|
||||
helpMsg.type = game::ChatType::SYSTEM;
|
||||
helpMsg.language = game::ChatLanguage::UNIVERSAL;
|
||||
helpMsg.message = line;
|
||||
ctx.gameHandler.addLocalChatMessage(helpMsg);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"chathelp"}; }
|
||||
std::string helpText() const override { return "List chat channel commands"; }
|
||||
};
|
||||
|
||||
// --- /macrohelp ---
|
||||
class MacroHelpCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
static constexpr const char* kMacroHelp[] = {
|
||||
"--- Macro Conditionals ---",
|
||||
"Usage: /cast [cond1,cond2] Spell1; [cond3] Spell2; Default",
|
||||
"State: [combat] [mounted] [swimming] [flying] [stealthed]",
|
||||
" [channeling] [pet] [group] [raid] [indoors] [outdoors]",
|
||||
"Spec: [spec:1] [spec:2] (active talent spec, 1-based)",
|
||||
" (prefix no- to negate any condition)",
|
||||
"Target: [harm] [help] [exists] [noexists] [dead] [nodead]",
|
||||
" [target=focus] [target=pet] [target=mouseover] [target=player]",
|
||||
" (also: @focus, @pet, @mouseover, @player, @target)",
|
||||
"Form: [noform] [nostance] [form:0]",
|
||||
"Keys: [mod:shift] [mod:ctrl] [mod:alt]",
|
||||
"Aura: [buff:Name] [nobuff:Name] [debuff:Name] [nodebuff:Name]",
|
||||
"Other: #showtooltip, /stopmacro [cond], /castsequence",
|
||||
};
|
||||
for (const char* line : kMacroHelp) {
|
||||
game::MessageChatData m;
|
||||
m.type = game::ChatType::SYSTEM;
|
||||
m.language = game::ChatLanguage::UNIVERSAL;
|
||||
m.message = line;
|
||||
ctx.gameHandler.addLocalChatMessage(m);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"macrohelp"}; }
|
||||
std::string helpText() const override { return "List macro conditionals"; }
|
||||
};
|
||||
|
||||
// --- Registration ---
|
||||
void registerHelpCommands(ChatCommandRegistry& reg) {
|
||||
reg.registerCommand(std::make_unique<HelpCommand>());
|
||||
reg.registerCommand(std::make_unique<ChatHelpCommand>());
|
||||
reg.registerCommand(std::make_unique<MacroHelpCommand>());
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
404
src/ui/chat/commands/misc_commands.cpp
Normal file
404
src/ui/chat/commands/misc_commands.cpp
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
// Misc commands: /time, /loc, /zone, /played, /screenshot, /ticket, /score,
|
||||
// /threat, /combatlog, /helm, /cloak, /follow, /stopfollow,
|
||||
// /assist, /pvp, /unstuck*, /transport
|
||||
// Moved from ChatPanel::sendChatMessage() if/else chain (Phase 3).
|
||||
#include "ui/chat/i_chat_command.hpp"
|
||||
#include "ui/chat_panel.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "game/entity.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <glm/vec3.hpp>
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
// Forward declaration of evaluateMacroConditionals (still in chat_panel_commands.cpp)
|
||||
std::string evaluateMacroConditionals(const std::string& rawArg,
|
||||
game::GameHandler& gameHandler,
|
||||
uint64_t& targetOverride);
|
||||
|
||||
namespace {
|
||||
|
||||
inline std::string getEntityName(const std::shared_ptr<game::Entity>& entity) {
|
||||
if (entity->getType() == game::ObjectType::PLAYER) {
|
||||
auto player = std::static_pointer_cast<game::Player>(entity);
|
||||
if (!player->getName().empty()) return player->getName();
|
||||
} else if (entity->getType() == game::ObjectType::UNIT) {
|
||||
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
||||
if (!unit->getName().empty()) return unit->getName();
|
||||
} else if (entity->getType() == game::ObjectType::GAMEOBJECT) {
|
||||
auto go = std::static_pointer_cast<game::GameObject>(entity);
|
||||
if (!go->getName().empty()) return go->getName();
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
} // anon namespace
|
||||
|
||||
// --- /time ---
|
||||
class TimeCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.queryServerTime();
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"time"}; }
|
||||
std::string helpText() const override { return "Query server time"; }
|
||||
};
|
||||
|
||||
// --- /loc, /coords, /whereami ---
|
||||
class LocCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
const auto& pmi = ctx.gameHandler.getMovementInfo();
|
||||
std::string zoneName;
|
||||
if (auto* rend = ctx.services.renderer)
|
||||
zoneName = rend->getCurrentZoneName();
|
||||
char buf[256];
|
||||
snprintf(buf, sizeof(buf), "%.1f, %.1f, %.1f%s%s",
|
||||
pmi.x, pmi.y, pmi.z,
|
||||
zoneName.empty() ? "" : " — ",
|
||||
zoneName.c_str());
|
||||
game::MessageChatData sysMsg;
|
||||
sysMsg.type = game::ChatType::SYSTEM;
|
||||
sysMsg.language = game::ChatLanguage::UNIVERSAL;
|
||||
sysMsg.message = buf;
|
||||
ctx.gameHandler.addLocalChatMessage(sysMsg);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"loc", "coords", "whereami"}; }
|
||||
std::string helpText() const override { return "Print player coordinates"; }
|
||||
};
|
||||
|
||||
// --- /zone ---
|
||||
class ZoneCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
std::string zoneName;
|
||||
if (auto* rend = ctx.services.renderer)
|
||||
zoneName = rend->getCurrentZoneName();
|
||||
game::MessageChatData sysMsg;
|
||||
sysMsg.type = game::ChatType::SYSTEM;
|
||||
sysMsg.language = game::ChatLanguage::UNIVERSAL;
|
||||
sysMsg.message = zoneName.empty() ? "You are not in a known zone." : "You are in: " + zoneName;
|
||||
ctx.gameHandler.addLocalChatMessage(sysMsg);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"zone"}; }
|
||||
std::string helpText() const override { return "Show current zone"; }
|
||||
};
|
||||
|
||||
// --- /played ---
|
||||
class PlayedCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.requestPlayedTime();
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"played"}; }
|
||||
std::string helpText() const override { return "Show time played"; }
|
||||
};
|
||||
|
||||
// --- /screenshot, /ss ---
|
||||
class ScreenshotCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.panel.getSlashCmds().takeScreenshot = true;
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"screenshot", "ss"}; }
|
||||
std::string helpText() const override { return "Take a screenshot"; }
|
||||
};
|
||||
|
||||
// --- /ticket, /gmticket, /gm ---
|
||||
class TicketCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.panel.getSlashCmds().showGmTicket = true;
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"ticket", "gmticket", "gm"}; }
|
||||
std::string helpText() const override { return "Open GM ticket"; }
|
||||
};
|
||||
|
||||
// --- /score ---
|
||||
class ScoreCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.requestPvpLog();
|
||||
ctx.panel.getSlashCmds().showBgScore = true;
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"score"}; }
|
||||
std::string helpText() const override { return "Show BG scoreboard"; }
|
||||
};
|
||||
|
||||
// --- /threat ---
|
||||
class ThreatCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.panel.getSlashCmds().toggleThreat = true;
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"threat"}; }
|
||||
std::string helpText() const override { return "Toggle threat display"; }
|
||||
};
|
||||
|
||||
// --- /combatlog, /cl ---
|
||||
class CombatLogCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.panel.getSlashCmds().toggleCombatLog = true;
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"combatlog", "cl"}; }
|
||||
std::string helpText() const override { return "Toggle combat log"; }
|
||||
};
|
||||
|
||||
// --- /helm, /helmet, /showhelm ---
|
||||
class HelmCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.toggleHelm();
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"helm", "helmet", "showhelm"}; }
|
||||
std::string helpText() const override { return "Toggle helmet visibility"; }
|
||||
};
|
||||
|
||||
// --- /cloak, /showcloak ---
|
||||
class CloakCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.toggleCloak();
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"cloak", "showcloak"}; }
|
||||
std::string helpText() const override { return "Toggle cloak visibility"; }
|
||||
};
|
||||
|
||||
// --- /follow, /f ---
|
||||
class FollowCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.followTarget();
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"follow", "f"}; }
|
||||
std::string helpText() const override { return "Follow target"; }
|
||||
};
|
||||
|
||||
// --- /stopfollow ---
|
||||
class StopFollowCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.cancelFollow();
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"stopfollow"}; }
|
||||
std::string helpText() const override { return "Stop following"; }
|
||||
};
|
||||
|
||||
// --- /assist ---
|
||||
class AssistCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
auto assistEntityTarget = [&](uint64_t srcGuid) {
|
||||
auto srcEnt = ctx.gameHandler.getEntityManager().getEntity(srcGuid);
|
||||
if (!srcEnt) { ctx.gameHandler.assistTarget(); return; }
|
||||
uint64_t atkGuid = 0;
|
||||
const auto& flds = srcEnt->getFields();
|
||||
auto iLo = flds.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_LO));
|
||||
if (iLo != flds.end()) {
|
||||
atkGuid = iLo->second;
|
||||
auto iHi = flds.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_HI));
|
||||
if (iHi != flds.end()) atkGuid |= (static_cast<uint64_t>(iHi->second) << 32);
|
||||
}
|
||||
if (atkGuid != 0) {
|
||||
ctx.gameHandler.setTarget(atkGuid);
|
||||
} else {
|
||||
std::string sn = getEntityName(srcEnt);
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = (sn.empty() ? "Target" : sn) + " has no target.";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
}
|
||||
};
|
||||
|
||||
if (!ctx.args.empty()) {
|
||||
std::string assistArg = ctx.args;
|
||||
while (!assistArg.empty() && assistArg.front() == ' ') assistArg.erase(assistArg.begin());
|
||||
|
||||
// Evaluate conditionals if present
|
||||
uint64_t assistOver = static_cast<uint64_t>(-1);
|
||||
if (!assistArg.empty() && assistArg.front() == '[') {
|
||||
assistArg = evaluateMacroConditionals(assistArg, ctx.gameHandler, assistOver);
|
||||
if (assistArg.empty() && assistOver == static_cast<uint64_t>(-1)) return {};
|
||||
while (!assistArg.empty() && assistArg.front() == ' ') assistArg.erase(assistArg.begin());
|
||||
while (!assistArg.empty() && assistArg.back() == ' ') assistArg.pop_back();
|
||||
}
|
||||
|
||||
if (assistOver != static_cast<uint64_t>(-1) && assistOver != 0) {
|
||||
assistEntityTarget(assistOver);
|
||||
} else if (!assistArg.empty()) {
|
||||
// Name search
|
||||
std::string argLow = assistArg;
|
||||
for (char& c : argLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
uint64_t bestGuid = 0; float bestDist = std::numeric_limits<float>::max();
|
||||
const auto& pmi = ctx.gameHandler.getMovementInfo();
|
||||
for (const auto& [guid, ent] : ctx.gameHandler.getEntityManager().getEntities()) {
|
||||
if (!ent || ent->getType() == game::ObjectType::OBJECT) continue;
|
||||
std::string nm = getEntityName(ent);
|
||||
std::string nml = nm;
|
||||
for (char& c : nml) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
if (nml.find(argLow) != 0) continue;
|
||||
float d2 = (ent->getX()-pmi.x)*(ent->getX()-pmi.x)
|
||||
+ (ent->getY()-pmi.y)*(ent->getY()-pmi.y);
|
||||
if (d2 < bestDist) { bestDist = d2; bestGuid = guid; }
|
||||
}
|
||||
if (bestGuid) assistEntityTarget(bestGuid);
|
||||
else {
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "No unit matching '" + assistArg + "' found.";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
}
|
||||
} else {
|
||||
ctx.gameHandler.assistTarget();
|
||||
}
|
||||
} else {
|
||||
ctx.gameHandler.assistTarget();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"assist"}; }
|
||||
std::string helpText() const override { return "Assist target (target their target)"; }
|
||||
};
|
||||
|
||||
// --- /pvp ---
|
||||
class PvpCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.togglePvp();
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"pvp"}; }
|
||||
std::string helpText() const override { return "Toggle PvP flag"; }
|
||||
};
|
||||
|
||||
// --- /unstuck ---
|
||||
class UnstuckCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.unstuck();
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"unstuck"}; }
|
||||
std::string helpText() const override { return "Reset position to floor height"; }
|
||||
};
|
||||
|
||||
// --- /unstuckgy ---
|
||||
class UnstuckGyCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.unstuckGy();
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"unstuckgy"}; }
|
||||
std::string helpText() const override { return "Move to nearest graveyard"; }
|
||||
};
|
||||
|
||||
// --- /unstuckhearth ---
|
||||
class UnstuckHearthCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.unstuckHearth();
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"unstuckhearth"}; }
|
||||
std::string helpText() const override { return "Teleport to hearthstone bind point"; }
|
||||
};
|
||||
|
||||
// --- /transport board ---
|
||||
class TransportBoardCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
// This is invoked via the "transport" alias. Check args for sub-command.
|
||||
std::string sub = ctx.args;
|
||||
while (!sub.empty() && sub.front() == ' ') sub.erase(sub.begin());
|
||||
for (char& c : sub) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
|
||||
if (sub == "board") {
|
||||
auto* tm = ctx.gameHandler.getTransportManager();
|
||||
if (tm) {
|
||||
uint64_t testTransportGuid = 0x1000000000000001ULL;
|
||||
glm::vec3 deckCenter(0.0f, 0.0f, 5.0f);
|
||||
ctx.gameHandler.setPlayerOnTransport(testTransportGuid, deckCenter);
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Boarded test transport. Use '/transport leave' to disembark.";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
} else {
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Transport system not available.";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
}
|
||||
return {};
|
||||
} else if (sub == "leave") {
|
||||
if (ctx.gameHandler.isOnTransport()) {
|
||||
ctx.gameHandler.clearPlayerTransport();
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Disembarked from transport.";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
} else {
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "You are not on a transport.";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
// Unrecognized sub-command
|
||||
return {false, false};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"transport"}; }
|
||||
std::string helpText() const override { return "Transport: /transport board|leave"; }
|
||||
};
|
||||
|
||||
// --- Registration ---
|
||||
void registerMiscCommands(ChatCommandRegistry& reg) {
|
||||
reg.registerCommand(std::make_unique<TimeCommand>());
|
||||
reg.registerCommand(std::make_unique<LocCommand>());
|
||||
reg.registerCommand(std::make_unique<ZoneCommand>());
|
||||
reg.registerCommand(std::make_unique<PlayedCommand>());
|
||||
reg.registerCommand(std::make_unique<ScreenshotCommand>());
|
||||
reg.registerCommand(std::make_unique<TicketCommand>());
|
||||
reg.registerCommand(std::make_unique<ScoreCommand>());
|
||||
reg.registerCommand(std::make_unique<ThreatCommand>());
|
||||
reg.registerCommand(std::make_unique<CombatLogCommand>());
|
||||
reg.registerCommand(std::make_unique<HelmCommand>());
|
||||
reg.registerCommand(std::make_unique<CloakCommand>());
|
||||
reg.registerCommand(std::make_unique<FollowCommand>());
|
||||
reg.registerCommand(std::make_unique<StopFollowCommand>());
|
||||
reg.registerCommand(std::make_unique<AssistCommand>());
|
||||
reg.registerCommand(std::make_unique<PvpCommand>());
|
||||
reg.registerCommand(std::make_unique<UnstuckCommand>());
|
||||
reg.registerCommand(std::make_unique<UnstuckGyCommand>());
|
||||
reg.registerCommand(std::make_unique<UnstuckHearthCommand>());
|
||||
reg.registerCommand(std::make_unique<TransportBoardCommand>());
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
211
src/ui/chat/commands/social_commands.cpp
Normal file
211
src/ui/chat/commands/social_commands.cpp
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
// Social commands: /friend, /removefriend, /ignore, /unignore, /invite, /inspect, /who
|
||||
// Moved from ChatPanel::sendChatMessage() if/else chain (Phase 3).
|
||||
#include "ui/chat/i_chat_command.hpp"
|
||||
#include "ui/chat_panel.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
// --- /invite ---
|
||||
class InviteCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (ctx.args.empty()) return {false, false};
|
||||
ctx.gameHandler.inviteToGroup(ctx.args);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"invite"}; }
|
||||
std::string helpText() const override { return "Invite player to group"; }
|
||||
};
|
||||
|
||||
// --- /inspect ---
|
||||
class InspectCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
ctx.gameHandler.inspectTarget();
|
||||
ctx.panel.getSlashCmds().showInspect = true;
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"inspect"}; }
|
||||
std::string helpText() const override { return "Inspect target's equipment"; }
|
||||
};
|
||||
|
||||
// --- /friend, /addfriend ---
|
||||
class FriendCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (!ctx.args.empty()) {
|
||||
size_t subCmdSpace = ctx.args.find(' ');
|
||||
if (ctx.fullCommand == "friend" && subCmdSpace != std::string::npos) {
|
||||
std::string subCmd = ctx.args.substr(0, subCmdSpace);
|
||||
std::transform(subCmd.begin(), subCmd.end(), subCmd.begin(), ::tolower);
|
||||
if (subCmd == "add") {
|
||||
ctx.gameHandler.addFriend(ctx.args.substr(subCmdSpace + 1));
|
||||
return {};
|
||||
} else if (subCmd == "remove" || subCmd == "delete" || subCmd == "rem") {
|
||||
ctx.gameHandler.removeFriend(ctx.args.substr(subCmdSpace + 1));
|
||||
return {};
|
||||
}
|
||||
} else {
|
||||
// /addfriend name or /friend name (assume add)
|
||||
ctx.gameHandler.addFriend(ctx.args);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Usage: /friend add <name> or /friend remove <name>";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"friend", "addfriend"}; }
|
||||
std::string helpText() const override { return "Add/remove friend"; }
|
||||
};
|
||||
|
||||
// --- /removefriend, /delfriend, /remfriend ---
|
||||
class RemoveFriendCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (!ctx.args.empty()) {
|
||||
ctx.gameHandler.removeFriend(ctx.args);
|
||||
return {};
|
||||
}
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Usage: /removefriend <name>";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"removefriend", "delfriend", "remfriend"}; }
|
||||
std::string helpText() const override { return "Remove friend"; }
|
||||
};
|
||||
|
||||
// --- /ignore ---
|
||||
class IgnoreCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (!ctx.args.empty()) {
|
||||
ctx.gameHandler.addIgnore(ctx.args);
|
||||
return {};
|
||||
}
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Usage: /ignore <name>";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"ignore"}; }
|
||||
std::string helpText() const override { return "Ignore player messages"; }
|
||||
};
|
||||
|
||||
// --- /unignore ---
|
||||
class UnignoreCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (!ctx.args.empty()) {
|
||||
ctx.gameHandler.removeIgnore(ctx.args);
|
||||
return {};
|
||||
}
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Usage: /unignore <name>";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"unignore"}; }
|
||||
std::string helpText() const override { return "Unignore player"; }
|
||||
};
|
||||
|
||||
// --- /who, /whois, /online, /players ---
|
||||
class WhoCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
std::string query = ctx.args;
|
||||
// Trim
|
||||
size_t first = query.find_first_not_of(" \t\r\n");
|
||||
if (first == std::string::npos) query.clear();
|
||||
else { size_t last = query.find_last_not_of(" \t\r\n"); query = query.substr(first, last - first + 1); }
|
||||
|
||||
if (ctx.fullCommand == "whois" && query.empty()) {
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Usage: /whois <playerName>";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
return {};
|
||||
}
|
||||
if (ctx.fullCommand == "who" && (query == "help" || query == "?")) {
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Who commands: /who [name/filter], /whois <name>, /online";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
return {};
|
||||
}
|
||||
ctx.gameHandler.queryWho(query);
|
||||
ctx.panel.getSlashCmds().showWho = true;
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"who", "whois", "online", "players"}; }
|
||||
std::string helpText() const override { return "List online players"; }
|
||||
};
|
||||
|
||||
// --- /duel ---
|
||||
class DuelCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (ctx.gameHandler.hasTarget()) {
|
||||
ctx.gameHandler.proposeDuel(ctx.gameHandler.getTargetGuid());
|
||||
} else {
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "You must target a player to challenge to a duel.";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"duel"}; }
|
||||
std::string helpText() const override { return "Challenge target to duel"; }
|
||||
};
|
||||
|
||||
// --- /trade ---
|
||||
class TradeCommand : public IChatCommand {
|
||||
public:
|
||||
ChatCommandResult execute(ChatCommandContext& ctx) override {
|
||||
if (ctx.gameHandler.hasTarget()) {
|
||||
ctx.gameHandler.initiateTrade(ctx.gameHandler.getTargetGuid());
|
||||
} else {
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "You must target a player to trade with.";
|
||||
ctx.gameHandler.addLocalChatMessage(msg);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> aliases() const override { return {"trade"}; }
|
||||
std::string helpText() const override { return "Initiate trade with target"; }
|
||||
};
|
||||
|
||||
// --- Registration ---
|
||||
void registerSocialCommands(ChatCommandRegistry& reg) {
|
||||
reg.registerCommand(std::make_unique<InviteCommand>());
|
||||
reg.registerCommand(std::make_unique<InspectCommand>());
|
||||
reg.registerCommand(std::make_unique<FriendCommand>());
|
||||
reg.registerCommand(std::make_unique<RemoveFriendCommand>());
|
||||
reg.registerCommand(std::make_unique<IgnoreCommand>());
|
||||
reg.registerCommand(std::make_unique<UnignoreCommand>());
|
||||
reg.registerCommand(std::make_unique<WhoCommand>());
|
||||
reg.registerCommand(std::make_unique<DuelCommand>());
|
||||
reg.registerCommand(std::make_unique<TradeCommand>());
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
191
src/ui/chat/commands/system_commands.cpp
Normal file
191
src/ui/chat/commands/system_commands.cpp
Normal 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
|
||||
248
src/ui/chat/commands/target_commands.cpp
Normal file
248
src/ui/chat/commands/target_commands.cpp
Normal 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
|
||||
114
src/ui/chat/game_state_adapter.cpp
Normal file
114
src/ui/chat/game_state_adapter.cpp
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
// GameStateAdapter — concrete IGameState wrapping GameHandler + Renderer.
|
||||
// Phase 4.2 of chat_panel_ref.md.
|
||||
#include "ui/chat/game_state_adapter.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "game/character.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
GameStateAdapter::GameStateAdapter(game::GameHandler& gameHandler,
|
||||
rendering::Renderer* renderer)
|
||||
: gameHandler_(gameHandler), renderer_(renderer) {}
|
||||
|
||||
// --- GUIDs ---
|
||||
uint64_t GameStateAdapter::getPlayerGuid() const { return gameHandler_.getPlayerGuid(); }
|
||||
uint64_t GameStateAdapter::getTargetGuid() const { return gameHandler_.getTargetGuid(); }
|
||||
uint64_t GameStateAdapter::getFocusGuid() const { return gameHandler_.getFocusGuid(); }
|
||||
uint64_t GameStateAdapter::getPetGuid() const { return gameHandler_.getPetGuid(); }
|
||||
uint64_t GameStateAdapter::getMouseoverGuid() const { return gameHandler_.getMouseoverGuid(); }
|
||||
|
||||
// --- Player state ---
|
||||
bool GameStateAdapter::isInCombat() const { return gameHandler_.isInCombat(); }
|
||||
bool GameStateAdapter::isMounted() const { return gameHandler_.isMounted(); }
|
||||
bool GameStateAdapter::isSwimming() const { return gameHandler_.isSwimming(); }
|
||||
bool GameStateAdapter::isFlying() const { return gameHandler_.isPlayerFlying(); }
|
||||
bool GameStateAdapter::isCasting() const { return gameHandler_.isCasting(); }
|
||||
bool GameStateAdapter::isChanneling() const { return gameHandler_.isChanneling(); }
|
||||
|
||||
bool GameStateAdapter::isStealthed() const {
|
||||
// Check UNIT_FLAG_SNEAKING (0x02000000) on player's Unit
|
||||
auto pe = gameHandler_.getEntityManager().getEntity(gameHandler_.getPlayerGuid());
|
||||
if (!pe) return false;
|
||||
auto pu = std::dynamic_pointer_cast<game::Unit>(pe);
|
||||
return pu && (pu->getUnitFlags() & 0x02000000u) != 0;
|
||||
}
|
||||
|
||||
bool GameStateAdapter::hasPet() const { return gameHandler_.hasPet(); }
|
||||
bool GameStateAdapter::isInGroup() const { return gameHandler_.isInGroup(); }
|
||||
|
||||
bool GameStateAdapter::isInRaid() const {
|
||||
return gameHandler_.isInGroup() && gameHandler_.getPartyData().groupType == 1;
|
||||
}
|
||||
|
||||
bool GameStateAdapter::isIndoors() const {
|
||||
return renderer_ && renderer_->isPlayerIndoors();
|
||||
}
|
||||
|
||||
// --- Numeric ---
|
||||
uint8_t GameStateAdapter::getActiveTalentSpec() const { return gameHandler_.getActiveTalentSpec(); }
|
||||
uint32_t GameStateAdapter::getVehicleId() const { return gameHandler_.getVehicleId(); }
|
||||
uint32_t GameStateAdapter::getCurrentCastSpellId() const { return gameHandler_.getCurrentCastSpellId(); }
|
||||
|
||||
// --- Spell/aura ---
|
||||
std::string GameStateAdapter::getSpellName(uint32_t spellId) const {
|
||||
return gameHandler_.getSpellName(spellId);
|
||||
}
|
||||
|
||||
bool GameStateAdapter::hasAuraByName(uint64_t targetGuid, const std::string& spellName,
|
||||
bool wantDebuff) const {
|
||||
// If targetGuid is player or invalid, check player auras; otherwise target auras
|
||||
const std::vector<game::AuraSlot>* auras = nullptr;
|
||||
uint64_t playerGuid = gameHandler_.getPlayerGuid();
|
||||
if (targetGuid != static_cast<uint64_t>(-1) && targetGuid != 0 &&
|
||||
targetGuid != playerGuid) {
|
||||
auras = &gameHandler_.getTargetAuras();
|
||||
} else {
|
||||
auras = &gameHandler_.getPlayerAuras();
|
||||
}
|
||||
|
||||
std::string nameLow = spellName;
|
||||
for (char& ch : nameLow) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||
|
||||
for (const auto& a : *auras) {
|
||||
if (a.isEmpty() || a.spellId == 0) continue;
|
||||
bool isDebuff = (a.flags & 0x80) != 0;
|
||||
if (wantDebuff ? !isDebuff : isDebuff) continue;
|
||||
std::string sn = gameHandler_.getSpellName(a.spellId);
|
||||
for (char& ch : sn) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||
if (sn == nameLow) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GameStateAdapter::hasFormAura() const {
|
||||
for (const auto& a : gameHandler_.getPlayerAuras()) {
|
||||
if (!a.isEmpty() && a.maxDurationMs == -1) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Entity queries ---
|
||||
bool GameStateAdapter::entityExists(uint64_t guid) const {
|
||||
if (guid == 0 || guid == static_cast<uint64_t>(-1)) return false;
|
||||
return gameHandler_.getEntityManager().getEntity(guid) != nullptr;
|
||||
}
|
||||
|
||||
bool GameStateAdapter::entityIsDead(uint64_t guid) const {
|
||||
auto entity = gameHandler_.getEntityManager().getEntity(guid);
|
||||
if (!entity) return false;
|
||||
auto unit = std::dynamic_pointer_cast<game::Unit>(entity);
|
||||
return unit && unit->getHealth() == 0;
|
||||
}
|
||||
|
||||
bool GameStateAdapter::entityIsHostile(uint64_t guid) const {
|
||||
auto entity = gameHandler_.getEntityManager().getEntity(guid);
|
||||
if (!entity) return false;
|
||||
auto unit = std::dynamic_pointer_cast<game::Unit>(entity);
|
||||
return unit && gameHandler_.isHostileFactionPublic(unit->getFactionTemplate());
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
28
src/ui/chat/input_modifier_adapter.cpp
Normal file
28
src/ui/chat/input_modifier_adapter.cpp
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// InputModifierAdapter — concrete IModifierState wrapping core::Input.
|
||||
// Phase 4.3 of chat_panel_ref.md.
|
||||
#include "ui/chat/input_modifier_adapter.hpp"
|
||||
#include "core/input.hpp"
|
||||
#include <SDL2/SDL_scancode.h>
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
bool InputModifierAdapter::isShiftHeld() const {
|
||||
auto& input = core::Input::getInstance();
|
||||
return input.isKeyPressed(SDL_SCANCODE_LSHIFT) ||
|
||||
input.isKeyPressed(SDL_SCANCODE_RSHIFT);
|
||||
}
|
||||
|
||||
bool InputModifierAdapter::isCtrlHeld() const {
|
||||
auto& input = core::Input::getInstance();
|
||||
return input.isKeyPressed(SDL_SCANCODE_LCTRL) ||
|
||||
input.isKeyPressed(SDL_SCANCODE_RCTRL);
|
||||
}
|
||||
|
||||
bool InputModifierAdapter::isAltHeld() const {
|
||||
auto& input = core::Input::getInstance();
|
||||
return input.isKeyPressed(SDL_SCANCODE_LALT) ||
|
||||
input.isKeyPressed(SDL_SCANCODE_RALT);
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
521
src/ui/chat/item_tooltip_renderer.cpp
Normal file
521
src/ui/chat/item_tooltip_renderer.cpp
Normal file
|
|
@ -0,0 +1,521 @@
|
|||
// ItemTooltipRenderer — renders full WoW-style item tooltips via ImGui.
|
||||
// Extracted from ChatMarkupRenderer::renderItemTooltip (Phase 6.7).
|
||||
#include "ui/chat/item_tooltip_renderer.hpp"
|
||||
#include "ui/ui_colors.hpp"
|
||||
#include "ui/inventory_screen.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "pipeline/dbc_layout.hpp"
|
||||
#include <imgui.h>
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
#include <cstdio>
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
void ItemTooltipRenderer::render(
|
||||
uint32_t itemEntry,
|
||||
game::GameHandler& gameHandler,
|
||||
InventoryScreen& inventoryScreen,
|
||||
pipeline::AssetManager* assetMgr)
|
||||
{
|
||||
const auto* info = gameHandler.getItemInfo(itemEntry);
|
||||
if (!info || !info->valid) return;
|
||||
|
||||
auto findComparableEquipped = [&](uint8_t inventoryType) -> const game::ItemSlot* {
|
||||
using ES = game::EquipSlot;
|
||||
const auto& inv = gameHandler.getInventory();
|
||||
auto slotPtr = [&](ES slot) -> const game::ItemSlot* {
|
||||
const auto& s = inv.getEquipSlot(slot);
|
||||
return s.empty() ? nullptr : &s;
|
||||
};
|
||||
switch (inventoryType) {
|
||||
case 1: return slotPtr(ES::HEAD);
|
||||
case 2: return slotPtr(ES::NECK);
|
||||
case 3: return slotPtr(ES::SHOULDERS);
|
||||
case 4: return slotPtr(ES::SHIRT);
|
||||
case 5:
|
||||
case 20: return slotPtr(ES::CHEST);
|
||||
case 6: return slotPtr(ES::WAIST);
|
||||
case 7: return slotPtr(ES::LEGS);
|
||||
case 8: return slotPtr(ES::FEET);
|
||||
case 9: return slotPtr(ES::WRISTS);
|
||||
case 10: return slotPtr(ES::HANDS);
|
||||
case 11: {
|
||||
if (auto* s = slotPtr(ES::RING1)) return s;
|
||||
return slotPtr(ES::RING2);
|
||||
}
|
||||
case 12: {
|
||||
if (auto* s = slotPtr(ES::TRINKET1)) return s;
|
||||
return slotPtr(ES::TRINKET2);
|
||||
}
|
||||
case 13:
|
||||
if (auto* s = slotPtr(ES::MAIN_HAND)) return s;
|
||||
return slotPtr(ES::OFF_HAND);
|
||||
case 14:
|
||||
case 22:
|
||||
case 23: return slotPtr(ES::OFF_HAND);
|
||||
case 15:
|
||||
case 25:
|
||||
case 26: return slotPtr(ES::RANGED);
|
||||
case 16: return slotPtr(ES::BACK);
|
||||
case 17:
|
||||
case 21: return slotPtr(ES::MAIN_HAND);
|
||||
case 18:
|
||||
for (int i = 0; i < game::Inventory::NUM_BAG_SLOTS; ++i) {
|
||||
auto slot = static_cast<ES>(static_cast<int>(ES::BAG1) + i);
|
||||
if (auto* s = slotPtr(slot)) return s;
|
||||
}
|
||||
return nullptr;
|
||||
case 19: return slotPtr(ES::TABARD);
|
||||
default: return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
auto isWeaponInventoryType = [](uint32_t invType) {
|
||||
switch (invType) {
|
||||
case 13: case 15: case 17: case 21: case 25: case 26: return true;
|
||||
default: return false;
|
||||
}
|
||||
};
|
||||
|
||||
auto appendBonus = [](std::string& out, int32_t val, const char* shortName) {
|
||||
if (val <= 0) return;
|
||||
if (!out.empty()) out += " ";
|
||||
out += "+" + std::to_string(val) + " ";
|
||||
out += shortName;
|
||||
};
|
||||
|
||||
ImGui::BeginTooltip();
|
||||
// Quality color for name
|
||||
auto qColor = ui::getQualityColor(static_cast<game::ItemQuality>(info->quality));
|
||||
ImGui::TextColored(qColor, "%s", info->name.c_str());
|
||||
|
||||
// Heroic indicator (green, matches WoW tooltip style)
|
||||
constexpr uint32_t kFlagHeroic = 0x8;
|
||||
constexpr uint32_t kFlagUniqueEquipped = 0x1000000;
|
||||
if (info->itemFlags & kFlagHeroic)
|
||||
ImGui::TextColored(ImVec4(0.0f, 0.8f, 0.0f, 1.0f), "Heroic");
|
||||
|
||||
// Bind type (appears right under name in WoW)
|
||||
switch (info->bindType) {
|
||||
case 1: ImGui::TextDisabled("Binds when picked up"); break;
|
||||
case 2: ImGui::TextDisabled("Binds when equipped"); break;
|
||||
case 3: ImGui::TextDisabled("Binds when used"); break;
|
||||
case 4: ImGui::TextDisabled("Quest Item"); break;
|
||||
}
|
||||
// Unique / Unique-Equipped
|
||||
if (info->maxCount == 1)
|
||||
ImGui::TextColored(ui::colors::kTooltipGold, "Unique");
|
||||
else if (info->itemFlags & kFlagUniqueEquipped)
|
||||
ImGui::TextColored(ui::colors::kTooltipGold, "Unique-Equipped");
|
||||
|
||||
// Slot type
|
||||
if (info->inventoryType > 0) {
|
||||
const char* slotName = ui::getInventorySlotName(info->inventoryType);
|
||||
if (slotName[0]) {
|
||||
if (!info->subclassName.empty())
|
||||
ImGui::TextColored(ui::colors::kLightGray, "%s %s", slotName, info->subclassName.c_str());
|
||||
else
|
||||
ImGui::TextColored(ui::colors::kLightGray, "%s", slotName);
|
||||
}
|
||||
}
|
||||
|
||||
const bool isWeapon = isWeaponInventoryType(info->inventoryType);
|
||||
|
||||
// Item level (after slot/subclass)
|
||||
if (info->itemLevel > 0)
|
||||
ImGui::TextDisabled("Item Level %u", info->itemLevel);
|
||||
|
||||
if (isWeapon && info->damageMax > 0.0f && info->delayMs > 0) {
|
||||
float speed = static_cast<float>(info->delayMs) / 1000.0f;
|
||||
float dps = ((info->damageMin + info->damageMax) * 0.5f) / speed;
|
||||
char dmgBuf[64], spdBuf[32];
|
||||
std::snprintf(dmgBuf, sizeof(dmgBuf), "%d - %d Damage",
|
||||
static_cast<int>(info->damageMin), static_cast<int>(info->damageMax));
|
||||
std::snprintf(spdBuf, sizeof(spdBuf), "Speed %.2f", speed);
|
||||
float spdW = ImGui::CalcTextSize(spdBuf).x;
|
||||
ImGui::Text("%s", dmgBuf);
|
||||
ImGui::SameLine(ImGui::GetWindowWidth() - spdW - 16.0f);
|
||||
ImGui::Text("%s", spdBuf);
|
||||
ImGui::TextDisabled("(%.1f damage per second)", dps);
|
||||
}
|
||||
ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f);
|
||||
std::string bonusLine;
|
||||
appendBonus(bonusLine, info->strength, "Str");
|
||||
appendBonus(bonusLine, info->agility, "Agi");
|
||||
appendBonus(bonusLine, info->stamina, "Sta");
|
||||
appendBonus(bonusLine, info->intellect, "Int");
|
||||
appendBonus(bonusLine, info->spirit, "Spi");
|
||||
if (!bonusLine.empty()) {
|
||||
ImGui::TextColored(green, "%s", bonusLine.c_str());
|
||||
}
|
||||
if (info->armor > 0) {
|
||||
ImGui::Text("%d Armor", info->armor);
|
||||
}
|
||||
// Elemental resistances
|
||||
{
|
||||
const int32_t resVals[6] = {
|
||||
info->holyRes, info->fireRes, info->natureRes,
|
||||
info->frostRes, info->shadowRes, info->arcaneRes
|
||||
};
|
||||
static constexpr const char* resLabels[6] = {
|
||||
"Holy Resistance", "Fire Resistance", "Nature Resistance",
|
||||
"Frost Resistance", "Shadow Resistance", "Arcane Resistance"
|
||||
};
|
||||
for (int ri = 0; ri < 6; ++ri)
|
||||
if (resVals[ri] > 0) ImGui::Text("+%d %s", resVals[ri], resLabels[ri]);
|
||||
}
|
||||
// Extra stats (hit/crit/haste/sp/ap/expertise/resilience/etc.)
|
||||
if (!info->extraStats.empty()) {
|
||||
auto statName = [](uint32_t t) -> const char* {
|
||||
switch (t) {
|
||||
case 12: return "Defense Rating";
|
||||
case 13: return "Dodge Rating";
|
||||
case 14: return "Parry Rating";
|
||||
case 15: return "Block Rating";
|
||||
case 16: case 17: case 18: case 31: return "Hit Rating";
|
||||
case 19: case 20: case 21: case 32: return "Critical Strike Rating";
|
||||
case 28: case 29: case 30: case 35: return "Haste Rating";
|
||||
case 34: return "Resilience Rating";
|
||||
case 36: return "Expertise Rating";
|
||||
case 37: return "Attack Power";
|
||||
case 38: return "Ranged Attack Power";
|
||||
case 45: return "Spell Power";
|
||||
case 46: return "Healing Power";
|
||||
case 47: return "Spell Damage";
|
||||
case 49: return "Mana per 5 sec.";
|
||||
case 43: return "Spell Penetration";
|
||||
case 44: return "Block Value";
|
||||
default: return nullptr;
|
||||
}
|
||||
};
|
||||
for (const auto& es : info->extraStats) {
|
||||
const char* nm = statName(es.statType);
|
||||
if (nm && es.statValue > 0)
|
||||
ImGui::TextColored(green, "+%d %s", es.statValue, nm);
|
||||
}
|
||||
}
|
||||
// Gem sockets
|
||||
{
|
||||
const auto& kSocketTypes = ui::kSocketTypes;
|
||||
bool hasSocket = false;
|
||||
for (int s = 0; s < 3; ++s) {
|
||||
if (info->socketColor[s] == 0) continue;
|
||||
if (!hasSocket) { ImGui::Spacing(); hasSocket = true; }
|
||||
for (const auto& st : kSocketTypes) {
|
||||
if (info->socketColor[s] & st.mask) {
|
||||
ImGui::TextColored(st.col, "%s", st.label);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasSocket && info->socketBonus != 0) {
|
||||
static std::unordered_map<uint32_t, std::string> s_enchantNames;
|
||||
static bool s_enchantNamesLoaded = false;
|
||||
if (!s_enchantNamesLoaded && assetMgr) {
|
||||
s_enchantNamesLoaded = true;
|
||||
auto dbc = assetMgr->loadDBC("SpellItemEnchantment.dbc");
|
||||
if (dbc && dbc->isLoaded()) {
|
||||
const auto* lay = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("SpellItemEnchantment") : nullptr;
|
||||
uint32_t nameField = lay ? lay->field("Name") : 8u;
|
||||
if (nameField == 0xFFFFFFFF) nameField = 8;
|
||||
uint32_t fc = dbc->getFieldCount();
|
||||
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
||||
uint32_t eid = dbc->getUInt32(r, 0);
|
||||
if (eid == 0 || nameField >= fc) continue;
|
||||
std::string ename = dbc->getString(r, nameField);
|
||||
if (!ename.empty()) s_enchantNames[eid] = std::move(ename);
|
||||
}
|
||||
}
|
||||
}
|
||||
auto enchIt = s_enchantNames.find(info->socketBonus);
|
||||
if (enchIt != s_enchantNames.end())
|
||||
ImGui::TextColored(colors::kSocketGreen, "Socket Bonus: %s", enchIt->second.c_str());
|
||||
else
|
||||
ImGui::TextColored(colors::kSocketGreen, "Socket Bonus: (id %u)", info->socketBonus);
|
||||
}
|
||||
}
|
||||
// Item set membership
|
||||
if (info->itemSetId != 0) {
|
||||
struct SetEntry {
|
||||
std::string name;
|
||||
std::array<uint32_t, 10> itemIds{};
|
||||
std::array<uint32_t, 10> spellIds{};
|
||||
std::array<uint32_t, 10> thresholds{};
|
||||
};
|
||||
static std::unordered_map<uint32_t, SetEntry> s_setData;
|
||||
static bool s_setDataLoaded = false;
|
||||
if (!s_setDataLoaded && assetMgr) {
|
||||
s_setDataLoaded = true;
|
||||
auto dbc = assetMgr->loadDBC("ItemSet.dbc");
|
||||
if (dbc && dbc->isLoaded()) {
|
||||
const auto* layout = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("ItemSet") : nullptr;
|
||||
auto lf = [&](const char* k, uint32_t def) -> uint32_t {
|
||||
return layout ? (*layout)[k] : def;
|
||||
};
|
||||
uint32_t idF = lf("ID", 0), nameF = lf("Name", 1);
|
||||
const auto& itemKeys = ui::kItemSetItemKeys;
|
||||
const auto& spellKeys = ui::kItemSetSpellKeys;
|
||||
const auto& thrKeys = ui::kItemSetThresholdKeys;
|
||||
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
||||
uint32_t id = dbc->getUInt32(r, idF);
|
||||
if (!id) continue;
|
||||
SetEntry e;
|
||||
e.name = dbc->getString(r, nameF);
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
e.itemIds[i] = dbc->getUInt32(r, layout ? (*layout)[itemKeys[i]] : uint32_t(18 + i));
|
||||
e.spellIds[i] = dbc->getUInt32(r, layout ? (*layout)[spellKeys[i]] : uint32_t(28 + i));
|
||||
e.thresholds[i] = dbc->getUInt32(r, layout ? (*layout)[thrKeys[i]] : uint32_t(38 + i));
|
||||
}
|
||||
s_setData[id] = std::move(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::Spacing();
|
||||
const auto& inv = gameHandler.getInventory();
|
||||
auto setIt = s_setData.find(info->itemSetId);
|
||||
if (setIt != s_setData.end()) {
|
||||
const SetEntry& se = setIt->second;
|
||||
int equipped = 0, total = 0;
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
if (se.itemIds[i] == 0) continue;
|
||||
++total;
|
||||
for (int sl = 0; sl < game::Inventory::NUM_EQUIP_SLOTS; sl++) {
|
||||
const auto& eq = inv.getEquipSlot(static_cast<game::EquipSlot>(sl));
|
||||
if (!eq.empty() && eq.item.itemId == se.itemIds[i]) { ++equipped; break; }
|
||||
}
|
||||
}
|
||||
if (total > 0)
|
||||
ImGui::TextColored(ui::colors::kTooltipGold,
|
||||
"%s (%d/%d)", se.name.empty() ? "Set" : se.name.c_str(), equipped, total);
|
||||
else if (!se.name.empty())
|
||||
ImGui::TextColored(ui::colors::kTooltipGold, "%s", se.name.c_str());
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
if (se.spellIds[i] == 0 || se.thresholds[i] == 0) continue;
|
||||
const std::string& bname = gameHandler.getSpellName(se.spellIds[i]);
|
||||
bool active = (equipped >= static_cast<int>(se.thresholds[i]));
|
||||
ImVec4 col = active ? colors::kActiveGreen : colors::kInactiveGray;
|
||||
if (!bname.empty())
|
||||
ImGui::TextColored(col, "(%u) %s", se.thresholds[i], bname.c_str());
|
||||
else
|
||||
ImGui::TextColored(col, "(%u) Set Bonus", se.thresholds[i]);
|
||||
}
|
||||
} else {
|
||||
ImGui::TextColored(ui::colors::kTooltipGold, "Set (id %u)", info->itemSetId);
|
||||
}
|
||||
}
|
||||
// Item spell effects (Use / Equip / Chance on Hit / Teaches)
|
||||
for (const auto& sp : info->spells) {
|
||||
if (sp.spellId == 0) continue;
|
||||
const char* triggerLabel = nullptr;
|
||||
switch (sp.spellTrigger) {
|
||||
case 0: triggerLabel = "Use"; break;
|
||||
case 1: triggerLabel = "Equip"; break;
|
||||
case 2: triggerLabel = "Chance on Hit"; break;
|
||||
case 5: triggerLabel = "Teaches"; break;
|
||||
}
|
||||
if (!triggerLabel) continue;
|
||||
const std::string& spDesc = gameHandler.getSpellDescription(sp.spellId);
|
||||
const std::string& spText = !spDesc.empty() ? spDesc
|
||||
: gameHandler.getSpellName(sp.spellId);
|
||||
if (!spText.empty()) {
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 300.0f);
|
||||
ImGui::TextColored(colors::kCyan,
|
||||
"%s: %s", triggerLabel, spText.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
}
|
||||
// Required level
|
||||
if (info->requiredLevel > 1)
|
||||
ImGui::TextDisabled("Requires Level %u", info->requiredLevel);
|
||||
// Required skill
|
||||
if (info->requiredSkill != 0 && info->requiredSkillRank > 0) {
|
||||
static std::unordered_map<uint32_t, std::string> s_skillNames;
|
||||
static bool s_skillNamesLoaded = false;
|
||||
if (!s_skillNamesLoaded && assetMgr) {
|
||||
s_skillNamesLoaded = true;
|
||||
auto dbc = assetMgr->loadDBC("SkillLine.dbc");
|
||||
if (dbc && dbc->isLoaded()) {
|
||||
const auto* layout = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("SkillLine") : nullptr;
|
||||
uint32_t idF = layout ? (*layout)["ID"] : 0u;
|
||||
uint32_t nameF = layout ? (*layout)["Name"] : 2u;
|
||||
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
||||
uint32_t sid = dbc->getUInt32(r, idF);
|
||||
if (!sid) continue;
|
||||
std::string sname = dbc->getString(r, nameF);
|
||||
if (!sname.empty()) s_skillNames[sid] = std::move(sname);
|
||||
}
|
||||
}
|
||||
}
|
||||
uint32_t playerSkillVal = 0;
|
||||
const auto& skills = gameHandler.getPlayerSkills();
|
||||
auto skPit = skills.find(info->requiredSkill);
|
||||
if (skPit != skills.end()) playerSkillVal = skPit->second.effectiveValue();
|
||||
bool meetsSkill = (playerSkillVal == 0 || playerSkillVal >= info->requiredSkillRank);
|
||||
ImVec4 skColor = meetsSkill ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : colors::kPaleRed;
|
||||
auto skIt = s_skillNames.find(info->requiredSkill);
|
||||
if (skIt != s_skillNames.end())
|
||||
ImGui::TextColored(skColor, "Requires %s (%u)", skIt->second.c_str(), info->requiredSkillRank);
|
||||
else
|
||||
ImGui::TextColored(skColor, "Requires Skill %u (%u)", info->requiredSkill, info->requiredSkillRank);
|
||||
}
|
||||
// Required reputation
|
||||
if (info->requiredReputationFaction != 0 && info->requiredReputationRank > 0) {
|
||||
static std::unordered_map<uint32_t, std::string> s_factionNames;
|
||||
static bool s_factionNamesLoaded = false;
|
||||
if (!s_factionNamesLoaded && assetMgr) {
|
||||
s_factionNamesLoaded = true;
|
||||
auto dbc = assetMgr->loadDBC("Faction.dbc");
|
||||
if (dbc && dbc->isLoaded()) {
|
||||
const auto* layout = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("Faction") : nullptr;
|
||||
uint32_t idF = layout ? (*layout)["ID"] : 0u;
|
||||
uint32_t nameF = layout ? (*layout)["Name"] : 20u;
|
||||
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
||||
uint32_t fid = dbc->getUInt32(r, idF);
|
||||
if (!fid) continue;
|
||||
std::string fname = dbc->getString(r, nameF);
|
||||
if (!fname.empty()) s_factionNames[fid] = std::move(fname);
|
||||
}
|
||||
}
|
||||
}
|
||||
static constexpr const char* kRepRankNames[] = {
|
||||
"Hated", "Hostile", "Unfriendly", "Neutral",
|
||||
"Friendly", "Honored", "Revered", "Exalted"
|
||||
};
|
||||
const char* rankName = (info->requiredReputationRank < 8)
|
||||
? kRepRankNames[info->requiredReputationRank] : "Unknown";
|
||||
auto fIt = s_factionNames.find(info->requiredReputationFaction);
|
||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.75f), "Requires %s with %s",
|
||||
rankName,
|
||||
fIt != s_factionNames.end() ? fIt->second.c_str() : "Unknown Faction");
|
||||
}
|
||||
// Class restriction
|
||||
if (info->allowableClass != 0) {
|
||||
const auto& kClasses = ui::kClassMasks;
|
||||
int matchCount = 0;
|
||||
for (const auto& kc : kClasses)
|
||||
if (info->allowableClass & kc.mask) ++matchCount;
|
||||
if (matchCount > 0 && matchCount < 10) {
|
||||
char classBuf[128] = "Classes: ";
|
||||
bool first = true;
|
||||
for (const auto& kc : kClasses) {
|
||||
if (!(info->allowableClass & kc.mask)) continue;
|
||||
if (!first) strncat(classBuf, ", ", sizeof(classBuf) - strlen(classBuf) - 1);
|
||||
strncat(classBuf, kc.name, sizeof(classBuf) - strlen(classBuf) - 1);
|
||||
first = false;
|
||||
}
|
||||
uint8_t pc = gameHandler.getPlayerClass();
|
||||
uint32_t pmask = (pc > 0 && pc <= 10) ? (1u << (pc - 1)) : 0u;
|
||||
bool playerAllowed = (pmask == 0 || (info->allowableClass & pmask));
|
||||
ImVec4 clColor = playerAllowed ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : colors::kPaleRed;
|
||||
ImGui::TextColored(clColor, "%s", classBuf);
|
||||
}
|
||||
}
|
||||
// Race restriction
|
||||
if (info->allowableRace != 0) {
|
||||
const auto& kRaces = ui::kRaceMasks;
|
||||
constexpr uint32_t kAllPlayable = 1|2|4|8|16|32|64|128|512|1024;
|
||||
if ((info->allowableRace & kAllPlayable) != kAllPlayable) {
|
||||
int matchCount = 0;
|
||||
for (const auto& kr : kRaces)
|
||||
if (info->allowableRace & kr.mask) ++matchCount;
|
||||
if (matchCount > 0) {
|
||||
char raceBuf[160] = "Races: ";
|
||||
bool first = true;
|
||||
for (const auto& kr : kRaces) {
|
||||
if (!(info->allowableRace & kr.mask)) continue;
|
||||
if (!first) strncat(raceBuf, ", ", sizeof(raceBuf) - strlen(raceBuf) - 1);
|
||||
strncat(raceBuf, kr.name, sizeof(raceBuf) - strlen(raceBuf) - 1);
|
||||
first = false;
|
||||
}
|
||||
uint8_t pr = gameHandler.getPlayerRace();
|
||||
uint32_t pmask = (pr > 0 && pr <= 11) ? (1u << (pr - 1)) : 0u;
|
||||
bool playerAllowed = (pmask == 0 || (info->allowableRace & pmask));
|
||||
ImVec4 rColor = playerAllowed ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : colors::kPaleRed;
|
||||
ImGui::TextColored(rColor, "%s", raceBuf);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Flavor text
|
||||
if (!info->description.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::PushTextWrapPos(300.0f);
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 0.85f), "\"%s\"", info->description.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
if (info->sellPrice > 0) {
|
||||
ImGui::TextDisabled("Sell:"); ImGui::SameLine(0, 4);
|
||||
renderCoinsFromCopper(info->sellPrice);
|
||||
}
|
||||
|
||||
if (ImGui::GetIO().KeyShift && info->inventoryType > 0) {
|
||||
if (const auto* eq = findComparableEquipped(static_cast<uint8_t>(info->inventoryType))) {
|
||||
ImGui::Separator();
|
||||
ImGui::TextDisabled("Equipped:");
|
||||
VkDescriptorSet eqIcon = inventoryScreen.getItemIcon(eq->item.displayInfoId);
|
||||
if (eqIcon) {
|
||||
ImGui::Image((ImTextureID)(uintptr_t)eqIcon, ImVec2(18.0f, 18.0f));
|
||||
ImGui::SameLine();
|
||||
}
|
||||
ImGui::TextColored(InventoryScreen::getQualityColor(eq->item.quality), "%s", eq->item.name.c_str());
|
||||
if (isWeaponInventoryType(eq->item.inventoryType) &&
|
||||
eq->item.damageMax > 0.0f && eq->item.delayMs > 0) {
|
||||
float speed = static_cast<float>(eq->item.delayMs) / 1000.0f;
|
||||
float dps = ((eq->item.damageMin + eq->item.damageMax) * 0.5f) / speed;
|
||||
char eqDmg[64], eqSpd[32];
|
||||
std::snprintf(eqDmg, sizeof(eqDmg), "%d - %d Damage",
|
||||
static_cast<int>(eq->item.damageMin), static_cast<int>(eq->item.damageMax));
|
||||
std::snprintf(eqSpd, sizeof(eqSpd), "Speed %.2f", speed);
|
||||
float eqSpdW = ImGui::CalcTextSize(eqSpd).x;
|
||||
ImGui::Text("%s", eqDmg);
|
||||
ImGui::SameLine(ImGui::GetWindowWidth() - eqSpdW - 16.0f);
|
||||
ImGui::Text("%s", eqSpd);
|
||||
ImGui::TextDisabled("(%.1f damage per second)", dps);
|
||||
}
|
||||
if (eq->item.armor > 0) {
|
||||
ImGui::Text("%d Armor", eq->item.armor);
|
||||
}
|
||||
std::string eqBonusLine;
|
||||
appendBonus(eqBonusLine, eq->item.strength, "Str");
|
||||
appendBonus(eqBonusLine, eq->item.agility, "Agi");
|
||||
appendBonus(eqBonusLine, eq->item.stamina, "Sta");
|
||||
appendBonus(eqBonusLine, eq->item.intellect, "Int");
|
||||
appendBonus(eqBonusLine, eq->item.spirit, "Spi");
|
||||
if (!eqBonusLine.empty()) {
|
||||
ImGui::TextColored(green, "%s", eqBonusLine.c_str());
|
||||
}
|
||||
// Extra stats for the equipped item
|
||||
for (const auto& es : eq->item.extraStats) {
|
||||
const char* nm = nullptr;
|
||||
switch (es.statType) {
|
||||
case 12: nm = "Defense Rating"; break;
|
||||
case 13: nm = "Dodge Rating"; break;
|
||||
case 14: nm = "Parry Rating"; break;
|
||||
case 16: case 17: case 18: case 31: nm = "Hit Rating"; break;
|
||||
case 19: case 20: case 21: case 32: nm = "Critical Strike Rating"; break;
|
||||
case 28: case 29: case 30: case 35: nm = "Haste Rating"; break;
|
||||
case 34: nm = "Resilience Rating"; break;
|
||||
case 36: nm = "Expertise Rating"; break;
|
||||
case 37: nm = "Attack Power"; break;
|
||||
case 38: nm = "Ranged Attack Power"; break;
|
||||
case 45: nm = "Spell Power"; break;
|
||||
case 46: nm = "Healing Power"; break;
|
||||
case 49: nm = "Mana per 5 sec."; break;
|
||||
default: break;
|
||||
}
|
||||
if (nm && es.statValue > 0)
|
||||
ImGui::TextColored(green, "+%d %s", es.statValue, nm);
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
24
src/ui/chat/macro_eval_convenience.cpp
Normal file
24
src/ui/chat/macro_eval_convenience.cpp
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// evaluateMacroConditionals — convenience free function.
|
||||
// Thin wrapper over MacroEvaluator with concrete adapters.
|
||||
// Separate TU to avoid pulling Application/Renderer into macro_evaluator unit tests.
|
||||
#include "ui/chat/macro_evaluator.hpp"
|
||||
#include "ui/chat/game_state_adapter.hpp"
|
||||
#include "ui/chat/input_modifier_adapter.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
std::string evaluateMacroConditionals(const std::string& rawArg,
|
||||
game::GameHandler& gameHandler,
|
||||
uint64_t& targetOverride) {
|
||||
auto* renderer = core::Application::getInstance().getRenderer();
|
||||
GameStateAdapter gs(gameHandler, renderer);
|
||||
InputModifierAdapter im;
|
||||
MacroEvaluator eval(gs, im);
|
||||
return eval.evaluate(rawArg, targetOverride);
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
223
src/ui/chat/macro_evaluator.cpp
Normal file
223
src/ui/chat/macro_evaluator.cpp
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
// MacroEvaluator — WoW macro conditional parser and evaluator.
|
||||
// Moved from evaluateMacroConditionals() in chat_panel_commands.cpp (Phase 4.4).
|
||||
#include "ui/chat/macro_evaluator.hpp"
|
||||
#include "ui/chat/i_game_state.hpp"
|
||||
#include "ui/chat/i_modifier_state.hpp"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
MacroEvaluator::MacroEvaluator(IGameState& gameState, IModifierState& modState)
|
||||
: gameState_(gameState), modState_(modState) {}
|
||||
|
||||
uint64_t MacroEvaluator::resolveEffectiveTarget(uint64_t tgt) const {
|
||||
if (tgt != static_cast<uint64_t>(-1) && tgt != 0)
|
||||
return tgt;
|
||||
return gameState_.getTargetGuid();
|
||||
}
|
||||
|
||||
bool MacroEvaluator::evalCondition(const std::string& raw, uint64_t& tgt) const {
|
||||
// Trim
|
||||
std::string c = raw;
|
||||
size_t s = c.find_first_not_of(" \t");
|
||||
c = (s != std::string::npos) ? c.substr(s) : "";
|
||||
size_t e = c.find_last_not_of(" \t");
|
||||
if (e != std::string::npos) c.resize(e + 1);
|
||||
if (c.empty()) return true;
|
||||
|
||||
// --- @target specifiers ---
|
||||
if (c[0] == '@') {
|
||||
std::string spec = c.substr(1);
|
||||
if (spec == "player") tgt = gameState_.getPlayerGuid();
|
||||
else if (spec == "focus") tgt = gameState_.getFocusGuid();
|
||||
else if (spec == "target") tgt = gameState_.getTargetGuid();
|
||||
else if (spec == "pet") {
|
||||
uint64_t pg = gameState_.getPetGuid();
|
||||
if (pg != 0) tgt = pg; else return false;
|
||||
}
|
||||
else if (spec == "mouseover") {
|
||||
uint64_t mo = gameState_.getMouseoverGuid();
|
||||
if (mo != 0) tgt = mo; else return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- target=X specifiers ---
|
||||
if (c.rfind("target=", 0) == 0) {
|
||||
std::string spec = c.substr(7);
|
||||
if (spec == "player") tgt = gameState_.getPlayerGuid();
|
||||
else if (spec == "focus") tgt = gameState_.getFocusGuid();
|
||||
else if (spec == "target") tgt = gameState_.getTargetGuid();
|
||||
else if (spec == "pet") {
|
||||
uint64_t pg = gameState_.getPetGuid();
|
||||
if (pg != 0) tgt = pg; else return false;
|
||||
}
|
||||
else if (spec == "mouseover") {
|
||||
uint64_t mo = gameState_.getMouseoverGuid();
|
||||
if (mo != 0) tgt = mo; else return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Modifier keys ---
|
||||
const bool shiftHeld = modState_.isShiftHeld();
|
||||
const bool ctrlHeld = modState_.isCtrlHeld();
|
||||
const bool altHeld = modState_.isAltHeld();
|
||||
const bool anyMod = shiftHeld || ctrlHeld || altHeld;
|
||||
|
||||
if (c == "nomod" || c == "mod:none") return !anyMod;
|
||||
if (c.rfind("mod:", 0) == 0) {
|
||||
std::string mods = c.substr(4);
|
||||
bool ok = true;
|
||||
if (mods.find("shift") != std::string::npos && !shiftHeld) ok = false;
|
||||
if (mods.find("ctrl") != std::string::npos && !ctrlHeld) ok = false;
|
||||
if (mods.find("alt") != std::string::npos && !altHeld) ok = false;
|
||||
return ok;
|
||||
}
|
||||
|
||||
// --- Combat ---
|
||||
if (c == "combat") return gameState_.isInCombat();
|
||||
if (c == "nocombat") return !gameState_.isInCombat();
|
||||
|
||||
// --- Effective target for exists/dead/help/harm ---
|
||||
uint64_t eff = resolveEffectiveTarget(tgt);
|
||||
|
||||
if (c == "exists") return gameState_.entityExists(eff);
|
||||
if (c == "noexists") return !gameState_.entityExists(eff);
|
||||
|
||||
if (c == "dead") return gameState_.entityIsDead(eff);
|
||||
if (c == "nodead") return !gameState_.entityIsDead(eff);
|
||||
|
||||
if (c == "harm" || c == "nohelp") return gameState_.entityIsHostile(eff);
|
||||
if (c == "help" || c == "noharm") return !gameState_.entityIsHostile(eff);
|
||||
|
||||
// --- Mounted / swimming / flying ---
|
||||
if (c == "mounted") return gameState_.isMounted();
|
||||
if (c == "nomounted") return !gameState_.isMounted();
|
||||
if (c == "swimming") return gameState_.isSwimming();
|
||||
if (c == "noswimming") return !gameState_.isSwimming();
|
||||
if (c == "flying") return gameState_.isFlying();
|
||||
if (c == "noflying") return !gameState_.isFlying();
|
||||
|
||||
// --- Channeling / casting ---
|
||||
if (c == "channeling") return gameState_.isCasting() && gameState_.isChanneling();
|
||||
if (c == "nochanneling") return !(gameState_.isCasting() && gameState_.isChanneling());
|
||||
if (c.rfind("channeling:", 0) == 0 && c.size() > 11) {
|
||||
if (!gameState_.isChanneling()) return false;
|
||||
std::string want = c.substr(11);
|
||||
for (char& ch : want) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||
uint32_t castSpellId = gameState_.getCurrentCastSpellId();
|
||||
std::string sn = gameState_.getSpellName(castSpellId);
|
||||
for (char& ch : sn) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||
return sn == want;
|
||||
}
|
||||
if (c == "casting") return gameState_.isCasting();
|
||||
if (c == "nocasting") return !gameState_.isCasting();
|
||||
|
||||
// --- Stealthed ---
|
||||
if (c == "stealthed") return gameState_.isStealthed();
|
||||
if (c == "nostealthed") return !gameState_.isStealthed();
|
||||
|
||||
// --- Pet ---
|
||||
if (c == "pet") return gameState_.hasPet();
|
||||
if (c == "nopet") return !gameState_.hasPet();
|
||||
|
||||
// --- Indoors / outdoors ---
|
||||
if (c == "indoors" || c == "nooutdoors") return gameState_.isIndoors();
|
||||
if (c == "outdoors" || c == "noindoors") return !gameState_.isIndoors();
|
||||
|
||||
// --- Group / raid ---
|
||||
if (c == "group" || c == "party") return gameState_.isInGroup();
|
||||
if (c == "nogroup") return !gameState_.isInGroup();
|
||||
if (c == "raid") return gameState_.isInRaid();
|
||||
if (c == "noraid") return !gameState_.isInRaid();
|
||||
|
||||
// --- Talent spec ---
|
||||
if (c.rfind("spec:", 0) == 0) {
|
||||
uint8_t wantSpec = 0;
|
||||
try { wantSpec = static_cast<uint8_t>(std::stoul(c.substr(5))); } catch (...) {}
|
||||
return wantSpec > 0 && gameState_.getActiveTalentSpec() == (wantSpec - 1);
|
||||
}
|
||||
|
||||
// --- Form / stance ---
|
||||
if (c == "noform" || c == "nostance" || c == "form:0" || c == "stance:0")
|
||||
return !gameState_.hasFormAura();
|
||||
|
||||
// --- Buff / debuff ---
|
||||
if (c.rfind("buff:", 0) == 0 && c.size() > 5)
|
||||
return gameState_.hasAuraByName(tgt, c.substr(5), false);
|
||||
if (c.rfind("nobuff:", 0) == 0 && c.size() > 7)
|
||||
return !gameState_.hasAuraByName(tgt, c.substr(7), false);
|
||||
if (c.rfind("debuff:", 0) == 0 && c.size() > 7)
|
||||
return gameState_.hasAuraByName(tgt, c.substr(7), true);
|
||||
if (c.rfind("nodebuff:", 0) == 0 && c.size() > 9)
|
||||
return !gameState_.hasAuraByName(tgt, c.substr(9), true);
|
||||
|
||||
// --- Vehicle ---
|
||||
if (c == "vehicle") return gameState_.getVehicleId() != 0;
|
||||
if (c == "novehicle") return gameState_.getVehicleId() == 0;
|
||||
|
||||
// Unknown → permissive (don't block)
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string MacroEvaluator::evaluate(const std::string& rawArg,
|
||||
uint64_t& targetOverride) const {
|
||||
targetOverride = static_cast<uint64_t>(-1);
|
||||
|
||||
// Split rawArg on ';' → alternatives
|
||||
std::vector<std::string> alts;
|
||||
{
|
||||
std::string cur;
|
||||
for (char ch : rawArg) {
|
||||
if (ch == ';') { alts.push_back(cur); cur.clear(); }
|
||||
else cur += ch;
|
||||
}
|
||||
alts.push_back(cur);
|
||||
}
|
||||
|
||||
for (auto& alt : alts) {
|
||||
// Trim
|
||||
size_t fs = alt.find_first_not_of(" \t");
|
||||
if (fs == std::string::npos) continue;
|
||||
alt = alt.substr(fs);
|
||||
size_t ls = alt.find_last_not_of(" \t");
|
||||
if (ls != std::string::npos) alt.resize(ls + 1);
|
||||
|
||||
if (!alt.empty() && alt[0] == '[') {
|
||||
size_t close = alt.find(']');
|
||||
if (close == std::string::npos) continue;
|
||||
std::string condStr = alt.substr(1, close - 1);
|
||||
std::string argPart = alt.substr(close + 1);
|
||||
// Trim argPart
|
||||
size_t as = argPart.find_first_not_of(" \t");
|
||||
argPart = (as != std::string::npos) ? argPart.substr(as) : "";
|
||||
|
||||
// Evaluate comma-separated conditions
|
||||
uint64_t tgt = static_cast<uint64_t>(-1);
|
||||
bool pass = true;
|
||||
size_t cp = 0;
|
||||
while (pass) {
|
||||
size_t comma = condStr.find(',', cp);
|
||||
std::string tok = condStr.substr(cp,
|
||||
comma == std::string::npos ? std::string::npos : comma - cp);
|
||||
if (!evalCondition(tok, tgt)) { pass = false; break; }
|
||||
if (comma == std::string::npos) break;
|
||||
cp = comma + 1;
|
||||
}
|
||||
if (pass) {
|
||||
if (tgt != static_cast<uint64_t>(-1)) targetOverride = tgt;
|
||||
return argPart;
|
||||
}
|
||||
} else {
|
||||
// No condition block — default fallback always matches
|
||||
return alt;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#include "ui/ui_colors.hpp"
|
||||
#include "ui/inventory_screen.hpp"
|
||||
#include "ui/keybinding_manager.hpp"
|
||||
#include "ui/chat/chat_utils.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "core/input.hpp"
|
||||
#include <imgui.h>
|
||||
|
|
@ -10,125 +11,6 @@
|
|||
namespace wowee { namespace ui {
|
||||
|
||||
namespace {
|
||||
// Helper function to replace gender placeholders, pronouns, and name
|
||||
std::string replaceGenderPlaceholders(const std::string& text, game::GameHandler& gameHandler) {
|
||||
game::Gender gender = game::Gender::NONBINARY;
|
||||
std::string playerName = "Adventurer";
|
||||
const auto* character = gameHandler.getActiveCharacter();
|
||||
if (character) {
|
||||
gender = character->gender;
|
||||
if (!character->name.empty()) {
|
||||
playerName = character->name;
|
||||
}
|
||||
}
|
||||
game::Pronouns pronouns = game::Pronouns::forGender(gender);
|
||||
|
||||
std::string result = text;
|
||||
|
||||
auto trim = [](std::string& s) {
|
||||
const char* ws = " \t\n\r";
|
||||
size_t start = s.find_first_not_of(ws);
|
||||
if (start == std::string::npos) { s.clear(); return; }
|
||||
size_t end = s.find_last_not_of(ws);
|
||||
s = s.substr(start, end - start + 1);
|
||||
};
|
||||
|
||||
// Replace $g placeholders
|
||||
size_t pos = 0;
|
||||
while ((pos = result.find('$', pos)) != std::string::npos) {
|
||||
if (pos + 1 >= result.length()) break;
|
||||
char marker = result[pos + 1];
|
||||
if (marker != 'g' && marker != 'G') { pos++; continue; }
|
||||
|
||||
size_t endPos = result.find(';', pos);
|
||||
if (endPos == std::string::npos) { pos += 2; continue; }
|
||||
|
||||
std::string placeholder = result.substr(pos + 2, endPos - pos - 2);
|
||||
|
||||
std::vector<std::string> parts;
|
||||
size_t start = 0;
|
||||
size_t colonPos;
|
||||
while ((colonPos = placeholder.find(':', start)) != std::string::npos) {
|
||||
std::string part = placeholder.substr(start, colonPos - start);
|
||||
trim(part);
|
||||
parts.push_back(part);
|
||||
start = colonPos + 1;
|
||||
}
|
||||
std::string lastPart = placeholder.substr(start);
|
||||
trim(lastPart);
|
||||
parts.push_back(lastPart);
|
||||
|
||||
std::string replacement;
|
||||
if (parts.size() >= 3) {
|
||||
switch (gender) {
|
||||
case game::Gender::MALE: replacement = parts[0]; break;
|
||||
case game::Gender::FEMALE: replacement = parts[1]; break;
|
||||
case game::Gender::NONBINARY: replacement = parts[2]; break;
|
||||
}
|
||||
} else if (parts.size() >= 2) {
|
||||
switch (gender) {
|
||||
case game::Gender::MALE: replacement = parts[0]; break;
|
||||
case game::Gender::FEMALE: replacement = parts[1]; break;
|
||||
case game::Gender::NONBINARY:
|
||||
replacement = parts[0].length() <= parts[1].length() ? parts[0] : parts[1];
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
pos = endPos + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
result.replace(pos, endPos - pos + 1, replacement);
|
||||
pos += replacement.length();
|
||||
}
|
||||
|
||||
// Resolve class and race names for $C and $R placeholders
|
||||
std::string className = "Adventurer";
|
||||
std::string raceName = "Unknown";
|
||||
if (character) {
|
||||
className = game::getClassName(character->characterClass);
|
||||
raceName = game::getRaceName(character->race);
|
||||
}
|
||||
|
||||
// Replace simple placeholders
|
||||
pos = 0;
|
||||
while ((pos = result.find('$', pos)) != std::string::npos) {
|
||||
if (pos + 1 >= result.length()) break;
|
||||
|
||||
char code = result[pos + 1];
|
||||
std::string replacement;
|
||||
|
||||
switch (code) {
|
||||
case 'n': case 'N': replacement = playerName; break;
|
||||
case 'c': case 'C': replacement = className; break;
|
||||
case 'r': case 'R': replacement = raceName; break;
|
||||
case 'p': replacement = pronouns.subject; break;
|
||||
case 'o': replacement = pronouns.object; break;
|
||||
case 's': replacement = pronouns.possessive; break;
|
||||
case 'S': replacement = pronouns.possessiveP; break;
|
||||
case 'b': case 'B': replacement = "\n"; break;
|
||||
case 'g': case 'G': pos++; continue;
|
||||
default: pos++; continue;
|
||||
}
|
||||
|
||||
result.replace(pos, 2, replacement);
|
||||
pos += replacement.length();
|
||||
}
|
||||
|
||||
// WoW markup linebreak token
|
||||
pos = 0;
|
||||
while ((pos = result.find("|n", pos)) != std::string::npos) {
|
||||
result.replace(pos, 2, "\n");
|
||||
pos += 1;
|
||||
}
|
||||
pos = 0;
|
||||
while ((pos = result.find("|N", pos)) != std::string::npos) {
|
||||
result.replace(pos, 2, "\n");
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string cleanQuestTitleForUi(const std::string& raw, uint32_t questId) {
|
||||
std::string s = raw;
|
||||
|
|
@ -446,7 +328,7 @@ void QuestLogScreen::render(game::GameHandler& gameHandler, InventoryScreen& inv
|
|||
if (lastDetailRequestQuestId_ == sel.questId) lastDetailRequestQuestId_ = 0;
|
||||
questDetailQueryNoResponse_.erase(sel.questId);
|
||||
ImGui::TextColored(ImVec4(0.82f, 0.9f, 1.0f, 1.0f), "Summary");
|
||||
std::string processedObjectives = replaceGenderPlaceholders(sel.objectives, gameHandler);
|
||||
std::string processedObjectives = chat_utils::replaceGenderPlaceholders(sel.objectives, gameHandler);
|
||||
float textHeight = ImGui::GetContentRegionAvail().y * 0.45f;
|
||||
if (textHeight < 120.0f) textHeight = 120.0f;
|
||||
ImGui::BeginChild("QuestObjectiveText", ImVec2(0, textHeight), true);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
// ============================================================
|
||||
#include "ui/window_manager.hpp"
|
||||
#include "ui/chat_panel.hpp"
|
||||
#include "ui/chat/chat_utils.hpp"
|
||||
#include "ui/settings_panel.hpp"
|
||||
#include "ui/spellbook_screen.hpp"
|
||||
#include "ui/inventory_screen.hpp"
|
||||
|
|
@ -252,7 +253,7 @@ void WindowManager::renderLootWindow(game::GameHandler& gameHandler,
|
|||
}
|
||||
|
||||
void WindowManager::renderGossipWindow(game::GameHandler& gameHandler,
|
||||
ChatPanel& chatPanel) {
|
||||
ChatPanel& /*chatPanel*/) {
|
||||
if (!gameHandler.isGossipWindowOpen()) return;
|
||||
|
||||
auto* window = services_.window;
|
||||
|
|
@ -330,7 +331,7 @@ void WindowManager::renderGossipWindow(game::GameHandler& gameHandler,
|
|||
displayText = placeholderIt->second;
|
||||
}
|
||||
|
||||
std::string processedText = chatPanel.replaceGenderPlaceholders(displayText, gameHandler);
|
||||
std::string processedText = chat_utils::replaceGenderPlaceholders(displayText, gameHandler);
|
||||
std::string label = std::string(icon) + " " + processedText;
|
||||
if (ImGui::Selectable(label.c_str())) {
|
||||
if (opt.text == "GOSSIP_OPTION_ARMORER") {
|
||||
|
|
@ -436,11 +437,11 @@ void WindowManager::renderQuestDetailsWindow(game::GameHandler& gameHandler,
|
|||
|
||||
bool open = true;
|
||||
const auto& quest = gameHandler.getQuestDetails();
|
||||
std::string processedTitle = chatPanel.replaceGenderPlaceholders(quest.title, gameHandler);
|
||||
std::string processedTitle = chat_utils::replaceGenderPlaceholders(quest.title, gameHandler);
|
||||
if (ImGui::Begin(processedTitle.c_str(), &open)) {
|
||||
// Quest description
|
||||
if (!quest.details.empty()) {
|
||||
std::string processedDetails = chatPanel.replaceGenderPlaceholders(quest.details, gameHandler);
|
||||
std::string processedDetails = chat_utils::replaceGenderPlaceholders(quest.details, gameHandler);
|
||||
ImGui::TextWrapped("%s", processedDetails.c_str());
|
||||
}
|
||||
|
||||
|
|
@ -449,7 +450,7 @@ void WindowManager::renderQuestDetailsWindow(game::GameHandler& gameHandler,
|
|||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ui::colors::kTooltipGold, "Objectives:");
|
||||
std::string processedObjectives = chatPanel.replaceGenderPlaceholders(quest.objectives, gameHandler);
|
||||
std::string processedObjectives = chat_utils::replaceGenderPlaceholders(quest.objectives, gameHandler);
|
||||
ImGui::TextWrapped("%s", processedObjectives.c_str());
|
||||
}
|
||||
|
||||
|
|
@ -577,10 +578,10 @@ void WindowManager::renderQuestRequestItemsWindow(game::GameHandler& gameHandler
|
|||
return total;
|
||||
};
|
||||
|
||||
std::string processedTitle = chatPanel.replaceGenderPlaceholders(quest.title, gameHandler);
|
||||
std::string processedTitle = chat_utils::replaceGenderPlaceholders(quest.title, gameHandler);
|
||||
if (ImGui::Begin(processedTitle.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
|
||||
if (!quest.completionText.empty()) {
|
||||
std::string processedCompletionText = chatPanel.replaceGenderPlaceholders(quest.completionText, gameHandler);
|
||||
std::string processedCompletionText = chat_utils::replaceGenderPlaceholders(quest.completionText, gameHandler);
|
||||
ImGui::TextWrapped("%s", processedCompletionText.c_str());
|
||||
}
|
||||
|
||||
|
|
@ -670,10 +671,10 @@ void WindowManager::renderQuestOfferRewardWindow(game::GameHandler& gameHandler,
|
|||
selectedChoice = 0;
|
||||
}
|
||||
|
||||
std::string processedTitle = chatPanel.replaceGenderPlaceholders(quest.title, gameHandler);
|
||||
std::string processedTitle = chat_utils::replaceGenderPlaceholders(quest.title, gameHandler);
|
||||
if (ImGui::Begin(processedTitle.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
|
||||
if (!quest.rewardText.empty()) {
|
||||
std::string processedRewardText = chatPanel.replaceGenderPlaceholders(quest.rewardText, gameHandler);
|
||||
std::string processedRewardText = chat_utils::replaceGenderPlaceholders(quest.rewardText, gameHandler);
|
||||
ImGui::TextWrapped("%s", processedRewardText.c_str());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
207
tests/test_chat_markup_parser.cpp
Normal file
207
tests/test_chat_markup_parser.cpp
Normal 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);
|
||||
}
|
||||
104
tests/test_chat_tab_completer.cpp
Normal file
104
tests/test_chat_tab_completer.cpp
Normal 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
128
tests/test_gm_commands.cpp
Normal 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"));
|
||||
}
|
||||
528
tests/test_macro_evaluator.cpp
Normal file
528
tests/test_macro_evaluator.cpp
Normal 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");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue