mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining full 3.3.5a private server compatibility: - Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature name queries, CMSG_SET_ACTIVE_MOVER after login - Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power tracking from UPDATE_OBJECT fields, floating combat text - Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar, cooldown tracking, aura/buff system with cancellation - Phase 4: Group invite/accept/decline/leave, party frames UI, /invite chat command - Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface Also: disable debug HUD/panels by default, gate 3D rendering to IN_GAME state only, fix window resize not updating UI positions.
This commit is contained in:
parent
6bf3fa4ed4
commit
c49bb58e47
14 changed files with 3039 additions and 84 deletions
|
|
@ -178,6 +178,10 @@ set(WOWEE_HEADERS
|
|||
include/game/zone_manager.hpp
|
||||
include/game/npc_manager.hpp
|
||||
include/game/inventory.hpp
|
||||
include/game/spell_defines.hpp
|
||||
include/game/group_defines.hpp
|
||||
include/game/world_packets.hpp
|
||||
include/game/character.hpp
|
||||
|
||||
include/audio/music_manager.hpp
|
||||
include/audio/footstep_manager.hpp
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ public:
|
|||
|
||||
int getWidth() const { return width; }
|
||||
int getHeight() const { return height; }
|
||||
void setSize(int w, int h) { width = w; height = h; }
|
||||
float getAspectRatio() const { return static_cast<float>(width) / height; }
|
||||
|
||||
SDL_Window* getSDLWindow() const { return window; }
|
||||
|
|
|
|||
|
|
@ -129,15 +129,33 @@ public:
|
|||
uint32_t getMaxHealth() const { return maxHealth; }
|
||||
void setMaxHealth(uint32_t h) { maxHealth = h; }
|
||||
|
||||
// Power (mana/rage/energy)
|
||||
uint32_t getPower() const { return power; }
|
||||
void setPower(uint32_t p) { power = p; }
|
||||
|
||||
uint32_t getMaxPower() const { return maxPower; }
|
||||
void setMaxPower(uint32_t p) { maxPower = p; }
|
||||
|
||||
uint8_t getPowerType() const { return powerType; }
|
||||
void setPowerType(uint8_t t) { powerType = t; }
|
||||
|
||||
// Level
|
||||
uint32_t getLevel() const { return level; }
|
||||
void setLevel(uint32_t l) { level = l; }
|
||||
|
||||
// Entry ID (creature template entry)
|
||||
uint32_t getEntry() const { return entry; }
|
||||
void setEntry(uint32_t e) { entry = e; }
|
||||
|
||||
protected:
|
||||
std::string name;
|
||||
uint32_t health = 0;
|
||||
uint32_t maxHealth = 0;
|
||||
uint32_t power = 0;
|
||||
uint32_t maxPower = 0;
|
||||
uint8_t powerType = 0; // 0=mana, 1=rage, 2=focus, 3=energy
|
||||
uint32_t level = 1;
|
||||
uint32_t entry = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,11 +3,16 @@
|
|||
#include "game/world_packets.hpp"
|
||||
#include "game/character.hpp"
|
||||
#include "game/inventory.hpp"
|
||||
#include "game/spell_defines.hpp"
|
||||
#include "game/group_defines.hpp"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace wowee {
|
||||
namespace network { class WorldSocket; class Packet; }
|
||||
|
|
@ -165,6 +170,77 @@ public:
|
|||
bool hasTarget() const { return targetGuid != 0; }
|
||||
void tabTarget(float playerX, float playerY, float playerZ);
|
||||
|
||||
// ---- Phase 1: Name queries ----
|
||||
void queryPlayerName(uint64_t guid);
|
||||
void queryCreatureInfo(uint32_t entry, uint64_t guid);
|
||||
std::string getCachedPlayerName(uint64_t guid) const;
|
||||
std::string getCachedCreatureName(uint32_t entry) const;
|
||||
|
||||
// ---- Phase 2: Combat ----
|
||||
void startAutoAttack(uint64_t targetGuid);
|
||||
void stopAutoAttack();
|
||||
bool isAutoAttacking() const { return autoAttacking; }
|
||||
const std::vector<CombatTextEntry>& getCombatText() const { return combatText; }
|
||||
void updateCombatText(float deltaTime);
|
||||
|
||||
// ---- Phase 3: Spells ----
|
||||
void castSpell(uint32_t spellId, uint64_t targetGuid = 0);
|
||||
void cancelCast();
|
||||
void cancelAura(uint32_t spellId);
|
||||
const std::vector<uint32_t>& getKnownSpells() const { return knownSpells; }
|
||||
bool isCasting() const { return casting; }
|
||||
uint32_t getCurrentCastSpellId() const { return currentCastSpellId; }
|
||||
float getCastProgress() const { return castTimeTotal > 0 ? (castTimeTotal - castTimeRemaining) / castTimeTotal : 0.0f; }
|
||||
float getCastTimeRemaining() const { return castTimeRemaining; }
|
||||
|
||||
// Action bar
|
||||
static constexpr int ACTION_BAR_SLOTS = 12;
|
||||
std::array<ActionBarSlot, ACTION_BAR_SLOTS>& getActionBar() { return actionBar; }
|
||||
const std::array<ActionBarSlot, ACTION_BAR_SLOTS>& getActionBar() const { return actionBar; }
|
||||
void setActionBarSlot(int slot, ActionBarSlot::Type type, uint32_t id);
|
||||
|
||||
// Auras
|
||||
const std::vector<AuraSlot>& getPlayerAuras() const { return playerAuras; }
|
||||
const std::vector<AuraSlot>& getTargetAuras() const { return targetAuras; }
|
||||
|
||||
// Cooldowns
|
||||
float getSpellCooldown(uint32_t spellId) const;
|
||||
|
||||
// Player GUID
|
||||
uint64_t getPlayerGuid() const { return playerGuid; }
|
||||
void setPlayerGuid(uint64_t guid) { playerGuid = guid; }
|
||||
|
||||
// ---- Phase 4: Group ----
|
||||
void inviteToGroup(const std::string& playerName);
|
||||
void acceptGroupInvite();
|
||||
void declineGroupInvite();
|
||||
void leaveGroup();
|
||||
bool isInGroup() const { return !partyData.isEmpty(); }
|
||||
const GroupListData& getPartyData() const { return partyData; }
|
||||
bool hasPendingGroupInvite() const { return pendingGroupInvite; }
|
||||
const std::string& getPendingInviterName() const { return pendingInviterName; }
|
||||
|
||||
// ---- Phase 5: Loot ----
|
||||
void lootTarget(uint64_t guid);
|
||||
void lootItem(uint8_t slotIndex);
|
||||
void closeLoot();
|
||||
bool isLootWindowOpen() const { return lootWindowOpen; }
|
||||
const LootResponseData& getCurrentLoot() const { return currentLoot; }
|
||||
|
||||
// NPC Gossip
|
||||
void interactWithNpc(uint64_t guid);
|
||||
void selectGossipOption(uint32_t optionId);
|
||||
void closeGossip();
|
||||
bool isGossipWindowOpen() const { return gossipWindowOpen; }
|
||||
const GossipMessageData& getCurrentGossip() const { return currentGossip; }
|
||||
|
||||
// Vendor
|
||||
void openVendor(uint64_t npcGuid);
|
||||
void buyItem(uint64_t vendorGuid, uint32_t itemId, uint32_t slot, uint8_t count);
|
||||
void sellItem(uint64_t vendorGuid, uint64_t itemGuid, uint8_t count);
|
||||
bool isVendorWindowOpen() const { return vendorWindowOpen; }
|
||||
const ListInventoryData& getVendorItems() const { return currentVendorItems; }
|
||||
|
||||
/**
|
||||
* Set callbacks
|
||||
*/
|
||||
|
|
@ -234,6 +310,45 @@ private:
|
|||
*/
|
||||
void handleMessageChat(network::Packet& packet);
|
||||
|
||||
// ---- Phase 1 handlers ----
|
||||
void handleNameQueryResponse(network::Packet& packet);
|
||||
void handleCreatureQueryResponse(network::Packet& packet);
|
||||
|
||||
// ---- Phase 2 handlers ----
|
||||
void handleAttackStart(network::Packet& packet);
|
||||
void handleAttackStop(network::Packet& packet);
|
||||
void handleAttackerStateUpdate(network::Packet& packet);
|
||||
void handleSpellDamageLog(network::Packet& packet);
|
||||
void handleSpellHealLog(network::Packet& packet);
|
||||
|
||||
// ---- Phase 3 handlers ----
|
||||
void handleInitialSpells(network::Packet& packet);
|
||||
void handleCastFailed(network::Packet& packet);
|
||||
void handleSpellStart(network::Packet& packet);
|
||||
void handleSpellGo(network::Packet& packet);
|
||||
void handleSpellCooldown(network::Packet& packet);
|
||||
void handleCooldownEvent(network::Packet& packet);
|
||||
void handleAuraUpdate(network::Packet& packet, bool isAll);
|
||||
void handleLearnedSpell(network::Packet& packet);
|
||||
void handleRemovedSpell(network::Packet& packet);
|
||||
|
||||
// ---- Phase 4 handlers ----
|
||||
void handleGroupInvite(network::Packet& packet);
|
||||
void handleGroupDecline(network::Packet& packet);
|
||||
void handleGroupList(network::Packet& packet);
|
||||
void handleGroupUninvite(network::Packet& packet);
|
||||
void handlePartyCommandResult(network::Packet& packet);
|
||||
|
||||
// ---- Phase 5 handlers ----
|
||||
void handleLootResponse(network::Packet& packet);
|
||||
void handleLootReleaseResponse(network::Packet& packet);
|
||||
void handleLootRemoved(network::Packet& packet);
|
||||
void handleGossipMessage(network::Packet& packet);
|
||||
void handleGossipComplete(network::Packet& packet);
|
||||
void handleListInventory(network::Packet& packet);
|
||||
|
||||
void addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId, bool isPlayerSource);
|
||||
|
||||
/**
|
||||
* Send CMSG_PING to server (heartbeat)
|
||||
*/
|
||||
|
|
@ -301,6 +416,49 @@ private:
|
|||
float pingInterval = 30.0f; // Ping interval (30 seconds)
|
||||
uint32_t lastLatency = 0; // Last measured latency (milliseconds)
|
||||
|
||||
// Player GUID
|
||||
uint64_t playerGuid = 0;
|
||||
|
||||
// ---- Phase 1: Name caches ----
|
||||
std::unordered_map<uint64_t, std::string> playerNameCache;
|
||||
std::unordered_set<uint64_t> pendingNameQueries;
|
||||
std::unordered_map<uint32_t, CreatureQueryResponseData> creatureInfoCache;
|
||||
std::unordered_set<uint32_t> pendingCreatureQueries;
|
||||
|
||||
// ---- Phase 2: Combat ----
|
||||
bool autoAttacking = false;
|
||||
uint64_t autoAttackTarget = 0;
|
||||
std::vector<CombatTextEntry> combatText;
|
||||
|
||||
// ---- Phase 3: Spells ----
|
||||
std::vector<uint32_t> knownSpells;
|
||||
std::unordered_map<uint32_t, float> spellCooldowns; // spellId -> remaining seconds
|
||||
uint8_t castCount = 0;
|
||||
bool casting = false;
|
||||
uint32_t currentCastSpellId = 0;
|
||||
float castTimeRemaining = 0.0f;
|
||||
float castTimeTotal = 0.0f;
|
||||
std::array<ActionBarSlot, 12> actionBar{};
|
||||
std::vector<AuraSlot> playerAuras;
|
||||
std::vector<AuraSlot> targetAuras;
|
||||
|
||||
// ---- Phase 4: Group ----
|
||||
GroupListData partyData;
|
||||
bool pendingGroupInvite = false;
|
||||
std::string pendingInviterName;
|
||||
|
||||
// ---- Phase 5: Loot ----
|
||||
bool lootWindowOpen = false;
|
||||
LootResponseData currentLoot;
|
||||
|
||||
// Gossip
|
||||
bool gossipWindowOpen = false;
|
||||
GossipMessageData currentGossip;
|
||||
|
||||
// Vendor
|
||||
bool vendorWindowOpen = false;
|
||||
ListInventoryData currentVendorItems;
|
||||
|
||||
// Callbacks
|
||||
WorldConnectSuccessCallback onSuccess;
|
||||
WorldConnectFailureCallback onFailure;
|
||||
|
|
|
|||
72
include/game/group_defines.hpp
Normal file
72
include/game/group_defines.hpp
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
||||
/**
|
||||
* Party/Group member data
|
||||
*/
|
||||
struct GroupMember {
|
||||
std::string name;
|
||||
uint64_t guid = 0;
|
||||
uint8_t isOnline = 0; // 0 = offline, 1 = online
|
||||
uint8_t subGroup = 0; // Raid subgroup (0 for party)
|
||||
uint8_t flags = 0; // Assistant, main tank, etc.
|
||||
uint8_t roles = 0; // LFG roles (3.3.5a)
|
||||
};
|
||||
|
||||
/**
|
||||
* Full group/party data from SMSG_GROUP_LIST
|
||||
*/
|
||||
struct GroupListData {
|
||||
uint8_t groupType = 0; // 0 = party, 1 = raid
|
||||
uint8_t subGroup = 0;
|
||||
uint8_t flags = 0;
|
||||
uint8_t roles = 0;
|
||||
uint8_t lootMethod = 0; // 0=free for all, 1=round robin, 2=master loot
|
||||
uint64_t looterGuid = 0;
|
||||
uint8_t lootThreshold = 0;
|
||||
uint8_t difficultyId = 0;
|
||||
uint8_t raidDifficultyId = 0;
|
||||
uint32_t memberCount = 0;
|
||||
std::vector<GroupMember> members;
|
||||
uint64_t leaderGuid = 0;
|
||||
|
||||
bool isValid() const { return true; }
|
||||
bool isEmpty() const { return memberCount == 0; }
|
||||
};
|
||||
|
||||
/**
|
||||
* Party command types
|
||||
*/
|
||||
enum class PartyCommand : uint32_t {
|
||||
INVITE = 0,
|
||||
UNINVITE = 1,
|
||||
LEAVE = 2,
|
||||
SWAP = 3
|
||||
};
|
||||
|
||||
/**
|
||||
* Party command result codes
|
||||
*/
|
||||
enum class PartyResult : uint32_t {
|
||||
OK = 0,
|
||||
BAD_PLAYER_NAME = 1,
|
||||
TARGET_NOT_IN_GROUP = 2,
|
||||
TARGET_NOT_IN_INSTANCE = 3,
|
||||
GROUP_FULL = 4,
|
||||
ALREADY_IN_GROUP = 5,
|
||||
NOT_IN_GROUP = 6,
|
||||
NOT_LEADER = 7,
|
||||
PLAYER_WRONG_FACTION = 8,
|
||||
IGNORING_YOU = 9,
|
||||
LFG_PENDING = 12,
|
||||
INVITE_RESTRICTED = 13
|
||||
};
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
|
|
@ -9,48 +9,132 @@ namespace game {
|
|||
// Values derived from community reverse-engineering efforts
|
||||
// Reference: https://wowdev.wiki/World_Packet
|
||||
enum class Opcode : uint16_t {
|
||||
// Client to Server
|
||||
CMSG_PING = 0x1DC,
|
||||
CMSG_AUTH_SESSION = 0x1ED,
|
||||
CMSG_CHAR_ENUM = 0x037,
|
||||
CMSG_PLAYER_LOGIN = 0x03D,
|
||||
// ---- Client to Server (Core) ----
|
||||
CMSG_PING = 0x1DC,
|
||||
CMSG_AUTH_SESSION = 0x1ED,
|
||||
CMSG_CHAR_ENUM = 0x037,
|
||||
CMSG_PLAYER_LOGIN = 0x03D,
|
||||
|
||||
// Movement
|
||||
CMSG_MOVE_START_FORWARD = 0x0B5,
|
||||
CMSG_MOVE_START_BACKWARD = 0x0B6,
|
||||
CMSG_MOVE_STOP = 0x0B7,
|
||||
CMSG_MOVE_START_STRAFE_LEFT = 0x0B8,
|
||||
CMSG_MOVE_START_STRAFE_RIGHT = 0x0B9,
|
||||
CMSG_MOVE_STOP_STRAFE = 0x0BA,
|
||||
CMSG_MOVE_JUMP = 0x0BB,
|
||||
CMSG_MOVE_START_TURN_LEFT = 0x0BC,
|
||||
CMSG_MOVE_START_TURN_RIGHT = 0x0BD,
|
||||
CMSG_MOVE_STOP_TURN = 0x0BE,
|
||||
CMSG_MOVE_SET_FACING = 0x0DA,
|
||||
CMSG_MOVE_FALL_LAND = 0x0C9,
|
||||
CMSG_MOVE_START_SWIM = 0x0CA,
|
||||
CMSG_MOVE_STOP_SWIM = 0x0CB,
|
||||
CMSG_MOVE_HEARTBEAT = 0x0EE,
|
||||
// ---- Movement ----
|
||||
CMSG_MOVE_START_FORWARD = 0x0B5,
|
||||
CMSG_MOVE_START_BACKWARD = 0x0B6,
|
||||
CMSG_MOVE_STOP = 0x0B7,
|
||||
CMSG_MOVE_START_STRAFE_LEFT = 0x0B8,
|
||||
CMSG_MOVE_START_STRAFE_RIGHT = 0x0B9,
|
||||
CMSG_MOVE_STOP_STRAFE = 0x0BA,
|
||||
CMSG_MOVE_JUMP = 0x0BB,
|
||||
CMSG_MOVE_START_TURN_LEFT = 0x0BC,
|
||||
CMSG_MOVE_START_TURN_RIGHT = 0x0BD,
|
||||
CMSG_MOVE_STOP_TURN = 0x0BE,
|
||||
CMSG_MOVE_SET_FACING = 0x0DA,
|
||||
CMSG_MOVE_FALL_LAND = 0x0C9,
|
||||
CMSG_MOVE_START_SWIM = 0x0CA,
|
||||
CMSG_MOVE_STOP_SWIM = 0x0CB,
|
||||
CMSG_MOVE_HEARTBEAT = 0x0EE,
|
||||
|
||||
// Server to Client
|
||||
SMSG_AUTH_CHALLENGE = 0x1EC,
|
||||
SMSG_AUTH_RESPONSE = 0x1EE,
|
||||
SMSG_CHAR_ENUM = 0x03B,
|
||||
SMSG_PONG = 0x1DD,
|
||||
SMSG_LOGIN_VERIFY_WORLD = 0x236,
|
||||
SMSG_ACCOUNT_DATA_TIMES = 0x209,
|
||||
SMSG_FEATURE_SYSTEM_STATUS = 0x3ED,
|
||||
SMSG_MOTD = 0x33D,
|
||||
// ---- Server to Client (Core) ----
|
||||
SMSG_AUTH_CHALLENGE = 0x1EC,
|
||||
SMSG_AUTH_RESPONSE = 0x1EE,
|
||||
SMSG_CHAR_ENUM = 0x03B,
|
||||
SMSG_PONG = 0x1DD,
|
||||
SMSG_LOGIN_VERIFY_WORLD = 0x236,
|
||||
SMSG_ACCOUNT_DATA_TIMES = 0x209,
|
||||
SMSG_FEATURE_SYSTEM_STATUS = 0x3ED,
|
||||
SMSG_MOTD = 0x33D,
|
||||
|
||||
// Entity/Object updates
|
||||
SMSG_UPDATE_OBJECT = 0x0A9,
|
||||
SMSG_DESTROY_OBJECT = 0x0AA,
|
||||
// ---- Entity/Object updates ----
|
||||
SMSG_UPDATE_OBJECT = 0x0A9,
|
||||
SMSG_DESTROY_OBJECT = 0x0AA,
|
||||
|
||||
// Chat
|
||||
CMSG_MESSAGECHAT = 0x095,
|
||||
SMSG_MESSAGECHAT = 0x096,
|
||||
// ---- Chat ----
|
||||
CMSG_MESSAGECHAT = 0x095,
|
||||
SMSG_MESSAGECHAT = 0x096,
|
||||
|
||||
// TODO: Add more opcodes as needed
|
||||
// ---- Phase 1: Foundation (Targeting, Queries) ----
|
||||
CMSG_SET_SELECTION = 0x13D,
|
||||
CMSG_NAME_QUERY = 0x050,
|
||||
SMSG_NAME_QUERY_RESPONSE = 0x051,
|
||||
CMSG_CREATURE_QUERY = 0x060,
|
||||
SMSG_CREATURE_QUERY_RESPONSE = 0x061,
|
||||
CMSG_GAMEOBJECT_QUERY = 0x05E,
|
||||
SMSG_GAMEOBJECT_QUERY_RESPONSE = 0x05F,
|
||||
CMSG_SET_ACTIVE_MOVER = 0x26A,
|
||||
|
||||
// ---- Phase 2: Combat Core ----
|
||||
CMSG_ATTACKSWING = 0x141,
|
||||
CMSG_ATTACKSTOP = 0x142,
|
||||
SMSG_ATTACKSTART = 0x143,
|
||||
SMSG_ATTACKSTOP = 0x144,
|
||||
SMSG_ATTACKERSTATEUPDATE = 0x14A,
|
||||
SMSG_SPELLNONMELEEDAMAGELOG = 0x250,
|
||||
SMSG_SPELLHEALLOG = 0x150,
|
||||
SMSG_SPELLENERGIZELOG = 0x25B,
|
||||
SMSG_PERIODICAURALOG = 0x24E,
|
||||
SMSG_ENVIRONMENTALDAMAGELOG = 0x1FC,
|
||||
|
||||
// ---- Phase 3: Spells, Action Bar, Auras ----
|
||||
CMSG_CAST_SPELL = 0x12E,
|
||||
CMSG_CANCEL_CAST = 0x12F,
|
||||
CMSG_CANCEL_AURA = 0x033,
|
||||
SMSG_CAST_FAILED = 0x130,
|
||||
SMSG_SPELL_START = 0x131,
|
||||
SMSG_SPELL_GO = 0x132,
|
||||
SMSG_SPELL_FAILURE = 0x133,
|
||||
SMSG_SPELL_COOLDOWN = 0x134,
|
||||
SMSG_COOLDOWN_EVENT = 0x135,
|
||||
SMSG_UPDATE_AURA_DURATION = 0x137,
|
||||
SMSG_INITIAL_SPELLS = 0x12A,
|
||||
SMSG_LEARNED_SPELL = 0x12B,
|
||||
SMSG_REMOVED_SPELL = 0x203,
|
||||
SMSG_SPELL_DELAYED = 0x1E2,
|
||||
SMSG_AURA_UPDATE = 0x3FA,
|
||||
SMSG_AURA_UPDATE_ALL = 0x495,
|
||||
SMSG_SET_FLAT_SPELL_MODIFIER = 0x266,
|
||||
SMSG_SET_PCT_SPELL_MODIFIER = 0x267,
|
||||
|
||||
// ---- Phase 4: Group/Party ----
|
||||
CMSG_GROUP_INVITE = 0x06E,
|
||||
SMSG_GROUP_INVITE = 0x06F,
|
||||
CMSG_GROUP_ACCEPT = 0x072,
|
||||
CMSG_GROUP_DECLINE = 0x073,
|
||||
SMSG_GROUP_DECLINE = 0x074,
|
||||
CMSG_GROUP_UNINVITE_GUID = 0x076,
|
||||
SMSG_GROUP_UNINVITE = 0x077,
|
||||
CMSG_GROUP_SET_LEADER = 0x078,
|
||||
SMSG_GROUP_SET_LEADER = 0x079,
|
||||
CMSG_GROUP_DISBAND = 0x07B,
|
||||
SMSG_GROUP_LIST = 0x07D,
|
||||
SMSG_PARTY_COMMAND_RESULT = 0x07E,
|
||||
MSG_RAID_TARGET_UPDATE = 0x321,
|
||||
|
||||
// ---- Phase 5: Loot ----
|
||||
CMSG_LOOT = 0x15D,
|
||||
CMSG_LOOT_RELEASE = 0x15E,
|
||||
SMSG_LOOT_RESPONSE = 0x160,
|
||||
SMSG_LOOT_RELEASE_RESPONSE = 0x161,
|
||||
CMSG_AUTOSTORE_LOOT_ITEM = 0x162,
|
||||
SMSG_LOOT_REMOVED = 0x163,
|
||||
SMSG_LOOT_MONEY_NOTIFY = 0x164,
|
||||
SMSG_LOOT_CLEAR_MONEY = 0x165,
|
||||
|
||||
// ---- Phase 5: NPC Gossip ----
|
||||
CMSG_GOSSIP_HELLO = 0x17C,
|
||||
SMSG_GOSSIP_MESSAGE = 0x17D,
|
||||
CMSG_GOSSIP_SELECT_OPTION = 0x17E,
|
||||
SMSG_GOSSIP_COMPLETE = 0x17F,
|
||||
SMSG_NPC_TEXT_UPDATE = 0x180,
|
||||
|
||||
// ---- Phase 5: Vendor ----
|
||||
CMSG_LIST_INVENTORY = 0x19E,
|
||||
SMSG_LIST_INVENTORY = 0x19F,
|
||||
CMSG_SELL_ITEM = 0x1A0,
|
||||
SMSG_SELL_ITEM = 0x1A1,
|
||||
CMSG_BUY_ITEM = 0x1A2,
|
||||
SMSG_BUY_FAILED = 0x1A5,
|
||||
|
||||
// ---- Phase 5: Item/Equip ----
|
||||
CMSG_AUTOEQUIP_ITEM = 0x10A,
|
||||
SMSG_INVENTORY_CHANGE_FAILURE = 0x112,
|
||||
};
|
||||
|
||||
} // namespace game
|
||||
|
|
|
|||
69
include/game/spell_defines.hpp
Normal file
69
include/game/spell_defines.hpp
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
||||
/**
|
||||
* Aura slot data for buff/debuff tracking
|
||||
*/
|
||||
struct AuraSlot {
|
||||
uint32_t spellId = 0;
|
||||
uint8_t flags = 0; // Active, positive/negative, etc.
|
||||
uint8_t level = 0;
|
||||
uint8_t charges = 0;
|
||||
int32_t durationMs = -1;
|
||||
int32_t maxDurationMs = -1;
|
||||
uint64_t casterGuid = 0;
|
||||
|
||||
bool isEmpty() const { return spellId == 0; }
|
||||
};
|
||||
|
||||
/**
|
||||
* Action bar slot
|
||||
*/
|
||||
struct ActionBarSlot {
|
||||
enum Type : uint8_t { EMPTY = 0, SPELL = 1, ITEM = 2, MACRO = 3 };
|
||||
Type type = EMPTY;
|
||||
uint32_t id = 0; // spellId, itemId, or macroId
|
||||
float cooldownRemaining = 0.0f;
|
||||
float cooldownTotal = 0.0f;
|
||||
|
||||
bool isReady() const { return cooldownRemaining <= 0.0f; }
|
||||
bool isEmpty() const { return type == EMPTY; }
|
||||
};
|
||||
|
||||
/**
|
||||
* Floating combat text entry
|
||||
*/
|
||||
struct CombatTextEntry {
|
||||
enum Type : uint8_t {
|
||||
MELEE_DAMAGE, SPELL_DAMAGE, HEAL, MISS, DODGE, PARRY, BLOCK,
|
||||
CRIT_DAMAGE, CRIT_HEAL, PERIODIC_DAMAGE, PERIODIC_HEAL, ENVIRONMENTAL
|
||||
};
|
||||
Type type;
|
||||
int32_t amount = 0;
|
||||
uint32_t spellId = 0;
|
||||
float age = 0.0f; // Seconds since creation (for fadeout)
|
||||
bool isPlayerSource = false; // True if player dealt this
|
||||
|
||||
static constexpr float LIFETIME = 2.5f;
|
||||
bool isExpired() const { return age >= LIFETIME; }
|
||||
};
|
||||
|
||||
/**
|
||||
* Spell cooldown entry received from server
|
||||
*/
|
||||
struct SpellCooldownEntry {
|
||||
uint32_t spellId;
|
||||
uint16_t itemId;
|
||||
uint16_t categoryId;
|
||||
uint32_t cooldownMs;
|
||||
uint32_t categoryCooldownMs;
|
||||
};
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
|
|
@ -3,10 +3,13 @@
|
|||
#include "network/packet.hpp"
|
||||
#include "game/opcodes.hpp"
|
||||
#include "game/entity.hpp"
|
||||
#include "game/spell_defines.hpp"
|
||||
#include "game/group_defines.hpp"
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
|
@ -397,6 +400,14 @@ public:
|
|||
*/
|
||||
static bool parse(network::Packet& packet, UpdateObjectData& data);
|
||||
|
||||
/**
|
||||
* Read packed GUID from packet
|
||||
*
|
||||
* @param packet Packet to read from
|
||||
* @return GUID value
|
||||
*/
|
||||
static uint64_t readPackedGuid(network::Packet& packet);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Parse a single update block
|
||||
|
|
@ -424,14 +435,6 @@ private:
|
|||
* @return true if successful
|
||||
*/
|
||||
static bool parseUpdateFields(network::Packet& packet, UpdateBlock& block);
|
||||
|
||||
/**
|
||||
* Read packed GUID from packet
|
||||
*
|
||||
* @param packet Packet to read from
|
||||
* @return GUID value
|
||||
*/
|
||||
static uint64_t readPackedGuid(network::Packet& packet);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -562,5 +565,521 @@ public:
|
|||
*/
|
||||
const char* getChatTypeString(ChatType type);
|
||||
|
||||
// ============================================================
|
||||
// Phase 1: Foundation — Targeting, Name Queries
|
||||
// ============================================================
|
||||
|
||||
/** CMSG_SET_SELECTION packet builder */
|
||||
class SetSelectionPacket {
|
||||
public:
|
||||
static network::Packet build(uint64_t targetGuid);
|
||||
};
|
||||
|
||||
/** CMSG_SET_ACTIVE_MOVER packet builder */
|
||||
class SetActiveMoverPacket {
|
||||
public:
|
||||
static network::Packet build(uint64_t guid);
|
||||
};
|
||||
|
||||
/** CMSG_NAME_QUERY packet builder */
|
||||
class NameQueryPacket {
|
||||
public:
|
||||
static network::Packet build(uint64_t playerGuid);
|
||||
};
|
||||
|
||||
/** SMSG_NAME_QUERY_RESPONSE data */
|
||||
struct NameQueryResponseData {
|
||||
uint64_t guid = 0;
|
||||
uint8_t found = 1; // 0 = found, 1 = not found
|
||||
std::string name;
|
||||
std::string realmName;
|
||||
uint8_t race = 0;
|
||||
uint8_t gender = 0;
|
||||
uint8_t classId = 0;
|
||||
|
||||
bool isValid() const { return found == 0 && !name.empty(); }
|
||||
};
|
||||
|
||||
/** SMSG_NAME_QUERY_RESPONSE parser */
|
||||
class NameQueryResponseParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, NameQueryResponseData& data);
|
||||
};
|
||||
|
||||
/** CMSG_CREATURE_QUERY packet builder */
|
||||
class CreatureQueryPacket {
|
||||
public:
|
||||
static network::Packet build(uint32_t entry, uint64_t guid);
|
||||
};
|
||||
|
||||
/** SMSG_CREATURE_QUERY_RESPONSE data */
|
||||
struct CreatureQueryResponseData {
|
||||
uint32_t entry = 0;
|
||||
std::string name;
|
||||
std::string subName;
|
||||
std::string iconName;
|
||||
uint32_t typeFlags = 0;
|
||||
uint32_t creatureType = 0;
|
||||
uint32_t family = 0;
|
||||
uint32_t rank = 0; // 0=Normal, 1=Elite, 2=Rare Elite, 3=Boss, 4=Rare
|
||||
|
||||
bool isValid() const { return entry != 0 && !name.empty(); }
|
||||
};
|
||||
|
||||
/** SMSG_CREATURE_QUERY_RESPONSE parser */
|
||||
class CreatureQueryResponseParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, CreatureQueryResponseData& data);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Phase 2: Combat Core
|
||||
// ============================================================
|
||||
|
||||
/** CMSG_ATTACKSWING packet builder */
|
||||
class AttackSwingPacket {
|
||||
public:
|
||||
static network::Packet build(uint64_t targetGuid);
|
||||
};
|
||||
|
||||
/** CMSG_ATTACKSTOP packet builder */
|
||||
class AttackStopPacket {
|
||||
public:
|
||||
static network::Packet build();
|
||||
};
|
||||
|
||||
/** SMSG_ATTACKSTART data */
|
||||
struct AttackStartData {
|
||||
uint64_t attackerGuid = 0;
|
||||
uint64_t victimGuid = 0;
|
||||
bool isValid() const { return attackerGuid != 0 && victimGuid != 0; }
|
||||
};
|
||||
|
||||
class AttackStartParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, AttackStartData& data);
|
||||
};
|
||||
|
||||
/** SMSG_ATTACKSTOP data */
|
||||
struct AttackStopData {
|
||||
uint64_t attackerGuid = 0;
|
||||
uint64_t victimGuid = 0;
|
||||
uint32_t unknown = 0;
|
||||
bool isValid() const { return true; }
|
||||
};
|
||||
|
||||
class AttackStopParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, AttackStopData& data);
|
||||
};
|
||||
|
||||
/** Sub-damage entry for melee hits */
|
||||
struct SubDamage {
|
||||
uint32_t schoolMask = 0;
|
||||
float damage = 0.0f;
|
||||
uint32_t intDamage = 0;
|
||||
uint32_t absorbed = 0;
|
||||
uint32_t resisted = 0;
|
||||
};
|
||||
|
||||
/** SMSG_ATTACKERSTATEUPDATE data */
|
||||
struct AttackerStateUpdateData {
|
||||
uint32_t hitInfo = 0;
|
||||
uint64_t attackerGuid = 0;
|
||||
uint64_t targetGuid = 0;
|
||||
int32_t totalDamage = 0;
|
||||
uint8_t subDamageCount = 0;
|
||||
std::vector<SubDamage> subDamages;
|
||||
uint32_t victimState = 0; // 0=hit, 1=dodge, 2=parry, 3=interrupt, 4=block, etc.
|
||||
int32_t overkill = -1;
|
||||
uint32_t blocked = 0;
|
||||
|
||||
bool isValid() const { return attackerGuid != 0; }
|
||||
bool isCrit() const { return (hitInfo & 0x200) != 0; }
|
||||
bool isMiss() const { return (hitInfo & 0x10) != 0; }
|
||||
};
|
||||
|
||||
class AttackerStateUpdateParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, AttackerStateUpdateData& data);
|
||||
};
|
||||
|
||||
/** SMSG_SPELLNONMELEEDAMAGELOG data (simplified) */
|
||||
struct SpellDamageLogData {
|
||||
uint64_t targetGuid = 0;
|
||||
uint64_t attackerGuid = 0;
|
||||
uint32_t spellId = 0;
|
||||
uint32_t damage = 0;
|
||||
uint32_t overkill = 0;
|
||||
uint8_t schoolMask = 0;
|
||||
uint32_t absorbed = 0;
|
||||
uint32_t resisted = 0;
|
||||
bool isCrit = false;
|
||||
|
||||
bool isValid() const { return spellId != 0; }
|
||||
};
|
||||
|
||||
class SpellDamageLogParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, SpellDamageLogData& data);
|
||||
};
|
||||
|
||||
/** SMSG_SPELLHEALLOG data (simplified) */
|
||||
struct SpellHealLogData {
|
||||
uint64_t targetGuid = 0;
|
||||
uint64_t casterGuid = 0;
|
||||
uint32_t spellId = 0;
|
||||
uint32_t heal = 0;
|
||||
uint32_t overheal = 0;
|
||||
uint32_t absorbed = 0;
|
||||
bool isCrit = false;
|
||||
|
||||
bool isValid() const { return spellId != 0; }
|
||||
};
|
||||
|
||||
class SpellHealLogParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, SpellHealLogData& data);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Phase 3: Spells, Action Bar, Auras
|
||||
// ============================================================
|
||||
|
||||
/** SMSG_INITIAL_SPELLS data */
|
||||
struct InitialSpellsData {
|
||||
uint8_t talentSpec = 0;
|
||||
std::vector<uint32_t> spellIds;
|
||||
std::vector<SpellCooldownEntry> cooldowns;
|
||||
|
||||
bool isValid() const { return true; }
|
||||
};
|
||||
|
||||
class InitialSpellsParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, InitialSpellsData& data);
|
||||
};
|
||||
|
||||
/** CMSG_CAST_SPELL packet builder */
|
||||
class CastSpellPacket {
|
||||
public:
|
||||
static network::Packet build(uint32_t spellId, uint64_t targetGuid, uint8_t castCount);
|
||||
};
|
||||
|
||||
/** CMSG_CANCEL_CAST packet builder */
|
||||
class CancelCastPacket {
|
||||
public:
|
||||
static network::Packet build(uint32_t spellId);
|
||||
};
|
||||
|
||||
/** CMSG_CANCEL_AURA packet builder */
|
||||
class CancelAuraPacket {
|
||||
public:
|
||||
static network::Packet build(uint32_t spellId);
|
||||
};
|
||||
|
||||
/** SMSG_CAST_FAILED data */
|
||||
struct CastFailedData {
|
||||
uint8_t castCount = 0;
|
||||
uint32_t spellId = 0;
|
||||
uint8_t result = 0;
|
||||
|
||||
bool isValid() const { return spellId != 0; }
|
||||
};
|
||||
|
||||
class CastFailedParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, CastFailedData& data);
|
||||
};
|
||||
|
||||
/** SMSG_SPELL_START data (simplified) */
|
||||
struct SpellStartData {
|
||||
uint64_t casterGuid = 0;
|
||||
uint64_t casterUnit = 0;
|
||||
uint8_t castCount = 0;
|
||||
uint32_t spellId = 0;
|
||||
uint32_t castFlags = 0;
|
||||
uint32_t castTime = 0;
|
||||
uint64_t targetGuid = 0;
|
||||
|
||||
bool isValid() const { return spellId != 0; }
|
||||
};
|
||||
|
||||
class SpellStartParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, SpellStartData& data);
|
||||
};
|
||||
|
||||
/** SMSG_SPELL_GO data (simplified) */
|
||||
struct SpellGoData {
|
||||
uint64_t casterGuid = 0;
|
||||
uint64_t casterUnit = 0;
|
||||
uint8_t castCount = 0;
|
||||
uint32_t spellId = 0;
|
||||
uint32_t castFlags = 0;
|
||||
uint8_t hitCount = 0;
|
||||
std::vector<uint64_t> hitTargets;
|
||||
uint8_t missCount = 0;
|
||||
|
||||
bool isValid() const { return spellId != 0; }
|
||||
};
|
||||
|
||||
class SpellGoParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, SpellGoData& data);
|
||||
};
|
||||
|
||||
/** SMSG_AURA_UPDATE / SMSG_AURA_UPDATE_ALL data */
|
||||
struct AuraUpdateData {
|
||||
uint64_t guid = 0;
|
||||
std::vector<std::pair<uint8_t, AuraSlot>> updates; // slot index + aura data
|
||||
|
||||
bool isValid() const { return true; }
|
||||
};
|
||||
|
||||
class AuraUpdateParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, AuraUpdateData& data, bool isAll);
|
||||
};
|
||||
|
||||
/** SMSG_SPELL_COOLDOWN data */
|
||||
struct SpellCooldownData {
|
||||
uint64_t guid = 0;
|
||||
uint8_t flags = 0;
|
||||
std::vector<std::pair<uint32_t, uint32_t>> cooldowns; // spellId, cooldownMs
|
||||
|
||||
bool isValid() const { return true; }
|
||||
};
|
||||
|
||||
class SpellCooldownParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, SpellCooldownData& data);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Phase 4: Group/Party System
|
||||
// ============================================================
|
||||
|
||||
/** CMSG_GROUP_INVITE packet builder */
|
||||
class GroupInvitePacket {
|
||||
public:
|
||||
static network::Packet build(const std::string& playerName);
|
||||
};
|
||||
|
||||
/** SMSG_GROUP_INVITE data */
|
||||
struct GroupInviteResponseData {
|
||||
uint8_t canAccept = 0;
|
||||
std::string inviterName;
|
||||
|
||||
bool isValid() const { return !inviterName.empty(); }
|
||||
};
|
||||
|
||||
class GroupInviteResponseParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, GroupInviteResponseData& data);
|
||||
};
|
||||
|
||||
/** CMSG_GROUP_ACCEPT packet builder */
|
||||
class GroupAcceptPacket {
|
||||
public:
|
||||
static network::Packet build();
|
||||
};
|
||||
|
||||
/** CMSG_GROUP_DECLINE packet builder */
|
||||
class GroupDeclinePacket {
|
||||
public:
|
||||
static network::Packet build();
|
||||
};
|
||||
|
||||
/** CMSG_GROUP_DISBAND (leave party) packet builder */
|
||||
class GroupDisbandPacket {
|
||||
public:
|
||||
static network::Packet build();
|
||||
};
|
||||
|
||||
/** SMSG_GROUP_LIST parser */
|
||||
class GroupListParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, GroupListData& data);
|
||||
};
|
||||
|
||||
/** SMSG_PARTY_COMMAND_RESULT data */
|
||||
struct PartyCommandResultData {
|
||||
PartyCommand command;
|
||||
std::string name;
|
||||
PartyResult result;
|
||||
|
||||
bool isValid() const { return true; }
|
||||
};
|
||||
|
||||
class PartyCommandResultParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, PartyCommandResultData& data);
|
||||
};
|
||||
|
||||
/** SMSG_GROUP_DECLINE data */
|
||||
struct GroupDeclineData {
|
||||
std::string playerName;
|
||||
bool isValid() const { return !playerName.empty(); }
|
||||
};
|
||||
|
||||
class GroupDeclineResponseParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, GroupDeclineData& data);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: Loot System
|
||||
// ============================================================
|
||||
|
||||
/** Loot item entry */
|
||||
struct LootItem {
|
||||
uint8_t slotIndex = 0;
|
||||
uint32_t itemId = 0;
|
||||
uint32_t count = 0;
|
||||
uint32_t displayInfoId = 0;
|
||||
uint32_t randomSuffix = 0;
|
||||
uint32_t randomPropertyId = 0;
|
||||
uint8_t lootSlotType = 0;
|
||||
};
|
||||
|
||||
/** SMSG_LOOT_RESPONSE data */
|
||||
struct LootResponseData {
|
||||
uint64_t lootGuid = 0;
|
||||
uint8_t lootType = 0;
|
||||
uint32_t gold = 0; // In copper
|
||||
std::vector<LootItem> items;
|
||||
|
||||
bool isValid() const { return true; }
|
||||
uint32_t getGold() const { return gold / 10000; }
|
||||
uint32_t getSilver() const { return (gold / 100) % 100; }
|
||||
uint32_t getCopper() const { return gold % 100; }
|
||||
};
|
||||
|
||||
/** CMSG_LOOT packet builder */
|
||||
class LootPacket {
|
||||
public:
|
||||
static network::Packet build(uint64_t targetGuid);
|
||||
};
|
||||
|
||||
/** CMSG_AUTOSTORE_LOOT_ITEM packet builder */
|
||||
class AutostoreLootItemPacket {
|
||||
public:
|
||||
static network::Packet build(uint8_t slotIndex);
|
||||
};
|
||||
|
||||
/** CMSG_LOOT_RELEASE packet builder */
|
||||
class LootReleasePacket {
|
||||
public:
|
||||
static network::Packet build(uint64_t lootGuid);
|
||||
};
|
||||
|
||||
/** SMSG_LOOT_RESPONSE parser */
|
||||
class LootResponseParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, LootResponseData& data);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: NPC Gossip
|
||||
// ============================================================
|
||||
|
||||
/** Gossip menu option */
|
||||
struct GossipOption {
|
||||
uint32_t id = 0;
|
||||
uint8_t icon = 0; // 0=chat, 1=vendor, 2=taxi, 3=trainer, etc.
|
||||
bool isCoded = false;
|
||||
uint32_t boxMoney = 0;
|
||||
std::string text;
|
||||
std::string boxText;
|
||||
};
|
||||
|
||||
/** Gossip quest item */
|
||||
struct GossipQuestItem {
|
||||
uint32_t questId = 0;
|
||||
uint32_t questIcon = 0;
|
||||
int32_t questLevel = 0;
|
||||
uint32_t questFlags = 0;
|
||||
uint8_t isRepeatable = 0;
|
||||
std::string title;
|
||||
};
|
||||
|
||||
/** SMSG_GOSSIP_MESSAGE data */
|
||||
struct GossipMessageData {
|
||||
uint64_t npcGuid = 0;
|
||||
uint32_t menuId = 0;
|
||||
uint32_t titleTextId = 0;
|
||||
std::vector<GossipOption> options;
|
||||
std::vector<GossipQuestItem> quests;
|
||||
|
||||
bool isValid() const { return true; }
|
||||
};
|
||||
|
||||
/** CMSG_GOSSIP_HELLO packet builder */
|
||||
class GossipHelloPacket {
|
||||
public:
|
||||
static network::Packet build(uint64_t npcGuid);
|
||||
};
|
||||
|
||||
/** CMSG_GOSSIP_SELECT_OPTION packet builder */
|
||||
class GossipSelectOptionPacket {
|
||||
public:
|
||||
static network::Packet build(uint64_t npcGuid, uint32_t optionId, const std::string& code = "");
|
||||
};
|
||||
|
||||
/** SMSG_GOSSIP_MESSAGE parser */
|
||||
class GossipMessageParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, GossipMessageData& data);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: Vendor
|
||||
// ============================================================
|
||||
|
||||
/** Vendor item entry */
|
||||
struct VendorItem {
|
||||
uint32_t slot = 0;
|
||||
uint32_t itemId = 0;
|
||||
uint32_t displayInfoId = 0;
|
||||
int32_t maxCount = -1; // -1 = unlimited
|
||||
uint32_t buyPrice = 0; // In copper
|
||||
uint32_t durability = 0;
|
||||
uint32_t stackCount = 0;
|
||||
uint32_t extendedCost = 0;
|
||||
};
|
||||
|
||||
/** SMSG_LIST_INVENTORY data */
|
||||
struct ListInventoryData {
|
||||
uint64_t vendorGuid = 0;
|
||||
std::vector<VendorItem> items;
|
||||
|
||||
bool isValid() const { return true; }
|
||||
};
|
||||
|
||||
/** CMSG_LIST_INVENTORY packet builder */
|
||||
class ListInventoryPacket {
|
||||
public:
|
||||
static network::Packet build(uint64_t npcGuid);
|
||||
};
|
||||
|
||||
/** CMSG_BUY_ITEM packet builder */
|
||||
class BuyItemPacket {
|
||||
public:
|
||||
static network::Packet build(uint64_t vendorGuid, uint32_t itemId, uint32_t slot, uint8_t count);
|
||||
};
|
||||
|
||||
/** CMSG_SELL_ITEM packet builder */
|
||||
class SellItemPacket {
|
||||
public:
|
||||
static network::Packet build(uint64_t vendorGuid, uint64_t itemGuid, uint8_t count);
|
||||
};
|
||||
|
||||
/** SMSG_LIST_INVENTORY parser */
|
||||
class ListInventoryParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, ListInventoryData& data);
|
||||
};
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ private:
|
|||
*/
|
||||
void calculateFPS();
|
||||
|
||||
bool enabled = true; // Enabled by default, press F1 to toggle
|
||||
bool enabled = false; // Disabled by default, press F1 to toggle
|
||||
Position position = Position::TOP_LEFT;
|
||||
|
||||
// Section visibility
|
||||
|
|
|
|||
|
|
@ -35,9 +35,9 @@ private:
|
|||
int selectedChatType = 0; // 0=SAY, 1=YELL, 2=PARTY, etc.
|
||||
|
||||
// UI state
|
||||
bool showEntityWindow = true;
|
||||
bool showEntityWindow = false;
|
||||
bool showChatWindow = true;
|
||||
bool showPlayerInfo = true;
|
||||
bool showPlayerInfo = false;
|
||||
bool refocusChatInput = false;
|
||||
|
||||
/**
|
||||
|
|
@ -95,6 +95,17 @@ private:
|
|||
*/
|
||||
void updateCharacterTextures(game::Inventory& inventory);
|
||||
|
||||
// ---- New UI renders ----
|
||||
void renderActionBar(game::GameHandler& gameHandler);
|
||||
void renderCastBar(game::GameHandler& gameHandler);
|
||||
void renderCombatText(game::GameHandler& gameHandler);
|
||||
void renderPartyFrames(game::GameHandler& gameHandler);
|
||||
void renderGroupInvitePopup(game::GameHandler& gameHandler);
|
||||
void renderBuffBar(game::GameHandler& gameHandler);
|
||||
void renderLootWindow(game::GameHandler& gameHandler);
|
||||
void renderGossipWindow(game::GameHandler& gameHandler);
|
||||
void renderVendorWindow(game::GameHandler& gameHandler);
|
||||
|
||||
/**
|
||||
* Inventory screen
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -189,6 +189,7 @@ void Application::run() {
|
|||
if (event.window.event == SDL_WINDOWEVENT_RESIZED) {
|
||||
int newWidth = event.window.data1;
|
||||
int newHeight = event.window.data2;
|
||||
window->setSize(newWidth, newHeight);
|
||||
glViewport(0, 0, newWidth, newHeight);
|
||||
if (renderer && renderer->getCamera()) {
|
||||
renderer->getCamera()->setAspectRatio(static_cast<float>(newWidth) / newHeight);
|
||||
|
|
@ -629,8 +630,8 @@ void Application::update(float deltaTime) {
|
|||
break;
|
||||
}
|
||||
|
||||
// Update renderer (camera, etc.)
|
||||
if (renderer) {
|
||||
// Update renderer (camera, etc.) only when in-game
|
||||
if (renderer && state == AppState::IN_GAME) {
|
||||
renderer->update(deltaTime);
|
||||
}
|
||||
|
||||
|
|
@ -647,25 +648,13 @@ void Application::render() {
|
|||
|
||||
renderer->beginFrame();
|
||||
|
||||
// Always render atmospheric background (sky, stars, clouds, etc.)
|
||||
// This provides a nice animated background for login/UI screens
|
||||
// Terrain/characters only render if loaded (in-game)
|
||||
switch (state) {
|
||||
case AppState::IN_GAME:
|
||||
// Render full world with terrain and entities
|
||||
if (world) {
|
||||
renderer->renderWorld(world.get());
|
||||
} else {
|
||||
// Fallback: render just atmosphere if world not loaded
|
||||
renderer->renderWorld(nullptr);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// Login/UI screens: render atmospheric background only
|
||||
// (terrain/water/characters won't render as they're not loaded)
|
||||
// Only render 3D world when in-game (after server connect or single-player)
|
||||
if (state == AppState::IN_GAME) {
|
||||
if (world) {
|
||||
renderer->renderWorld(world.get());
|
||||
} else {
|
||||
renderer->renderWorld(nullptr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Render performance HUD (within ImGui frame, before UI ends the frame)
|
||||
|
|
|
|||
|
|
@ -106,6 +106,37 @@ void GameHandler::update(float deltaTime) {
|
|||
sendPing();
|
||||
timeSinceLastPing = 0.0f;
|
||||
}
|
||||
|
||||
// Update cast timer (Phase 3)
|
||||
if (casting && castTimeRemaining > 0.0f) {
|
||||
castTimeRemaining -= deltaTime;
|
||||
if (castTimeRemaining <= 0.0f) {
|
||||
casting = false;
|
||||
currentCastSpellId = 0;
|
||||
castTimeRemaining = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
// Update spell cooldowns (Phase 3)
|
||||
for (auto it = spellCooldowns.begin(); it != spellCooldowns.end(); ) {
|
||||
it->second -= deltaTime;
|
||||
if (it->second <= 0.0f) {
|
||||
it = spellCooldowns.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
// Update action bar cooldowns
|
||||
for (auto& slot : actionBar) {
|
||||
if (slot.cooldownRemaining > 0.0f) {
|
||||
slot.cooldownRemaining -= deltaTime;
|
||||
if (slot.cooldownRemaining < 0.0f) slot.cooldownRemaining = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
// Update combat text (Phase 2)
|
||||
updateCombatText(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -192,6 +223,127 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
break;
|
||||
|
||||
// ---- Phase 1: Foundation ----
|
||||
case Opcode::SMSG_NAME_QUERY_RESPONSE:
|
||||
handleNameQueryResponse(packet);
|
||||
break;
|
||||
|
||||
case Opcode::SMSG_CREATURE_QUERY_RESPONSE:
|
||||
handleCreatureQueryResponse(packet);
|
||||
break;
|
||||
|
||||
// ---- Phase 2: Combat ----
|
||||
case Opcode::SMSG_ATTACKSTART:
|
||||
handleAttackStart(packet);
|
||||
break;
|
||||
case Opcode::SMSG_ATTACKSTOP:
|
||||
handleAttackStop(packet);
|
||||
break;
|
||||
case Opcode::SMSG_ATTACKERSTATEUPDATE:
|
||||
handleAttackerStateUpdate(packet);
|
||||
break;
|
||||
case Opcode::SMSG_SPELLNONMELEEDAMAGELOG:
|
||||
handleSpellDamageLog(packet);
|
||||
break;
|
||||
case Opcode::SMSG_SPELLHEALLOG:
|
||||
handleSpellHealLog(packet);
|
||||
break;
|
||||
|
||||
// ---- Phase 3: Spells ----
|
||||
case Opcode::SMSG_INITIAL_SPELLS:
|
||||
handleInitialSpells(packet);
|
||||
break;
|
||||
case Opcode::SMSG_CAST_FAILED:
|
||||
handleCastFailed(packet);
|
||||
break;
|
||||
case Opcode::SMSG_SPELL_START:
|
||||
handleSpellStart(packet);
|
||||
break;
|
||||
case Opcode::SMSG_SPELL_GO:
|
||||
handleSpellGo(packet);
|
||||
break;
|
||||
case Opcode::SMSG_SPELL_FAILURE:
|
||||
// Spell failed mid-cast
|
||||
casting = false;
|
||||
currentCastSpellId = 0;
|
||||
break;
|
||||
case Opcode::SMSG_SPELL_COOLDOWN:
|
||||
handleSpellCooldown(packet);
|
||||
break;
|
||||
case Opcode::SMSG_COOLDOWN_EVENT:
|
||||
handleCooldownEvent(packet);
|
||||
break;
|
||||
case Opcode::SMSG_AURA_UPDATE:
|
||||
handleAuraUpdate(packet, false);
|
||||
break;
|
||||
case Opcode::SMSG_AURA_UPDATE_ALL:
|
||||
handleAuraUpdate(packet, true);
|
||||
break;
|
||||
case Opcode::SMSG_LEARNED_SPELL:
|
||||
handleLearnedSpell(packet);
|
||||
break;
|
||||
case Opcode::SMSG_REMOVED_SPELL:
|
||||
handleRemovedSpell(packet);
|
||||
break;
|
||||
|
||||
// ---- Phase 4: Group ----
|
||||
case Opcode::SMSG_GROUP_INVITE:
|
||||
handleGroupInvite(packet);
|
||||
break;
|
||||
case Opcode::SMSG_GROUP_DECLINE:
|
||||
handleGroupDecline(packet);
|
||||
break;
|
||||
case Opcode::SMSG_GROUP_LIST:
|
||||
handleGroupList(packet);
|
||||
break;
|
||||
case Opcode::SMSG_GROUP_UNINVITE:
|
||||
handleGroupUninvite(packet);
|
||||
break;
|
||||
case Opcode::SMSG_PARTY_COMMAND_RESULT:
|
||||
handlePartyCommandResult(packet);
|
||||
break;
|
||||
|
||||
// ---- Phase 5: Loot/Gossip/Vendor ----
|
||||
case Opcode::SMSG_LOOT_RESPONSE:
|
||||
handleLootResponse(packet);
|
||||
break;
|
||||
case Opcode::SMSG_LOOT_RELEASE_RESPONSE:
|
||||
handleLootReleaseResponse(packet);
|
||||
break;
|
||||
case Opcode::SMSG_LOOT_REMOVED:
|
||||
handleLootRemoved(packet);
|
||||
break;
|
||||
case Opcode::SMSG_GOSSIP_MESSAGE:
|
||||
handleGossipMessage(packet);
|
||||
break;
|
||||
case Opcode::SMSG_GOSSIP_COMPLETE:
|
||||
handleGossipComplete(packet);
|
||||
break;
|
||||
case Opcode::SMSG_LIST_INVENTORY:
|
||||
handleListInventory(packet);
|
||||
break;
|
||||
|
||||
// Silently ignore common packets we don't handle yet
|
||||
case Opcode::SMSG_FEATURE_SYSTEM_STATUS:
|
||||
case Opcode::SMSG_SET_FLAT_SPELL_MODIFIER:
|
||||
case Opcode::SMSG_SET_PCT_SPELL_MODIFIER:
|
||||
case Opcode::SMSG_SPELL_DELAYED:
|
||||
case Opcode::SMSG_UPDATE_AURA_DURATION:
|
||||
case Opcode::SMSG_PERIODICAURALOG:
|
||||
case Opcode::SMSG_SPELLENERGIZELOG:
|
||||
case Opcode::SMSG_ENVIRONMENTALDAMAGELOG:
|
||||
case Opcode::SMSG_LOOT_MONEY_NOTIFY:
|
||||
case Opcode::SMSG_LOOT_CLEAR_MONEY:
|
||||
case Opcode::SMSG_NPC_TEXT_UPDATE:
|
||||
case Opcode::SMSG_SELL_ITEM:
|
||||
case Opcode::SMSG_BUY_FAILED:
|
||||
case Opcode::SMSG_INVENTORY_CHANGE_FAILURE:
|
||||
case Opcode::SMSG_GAMEOBJECT_QUERY_RESPONSE:
|
||||
case Opcode::MSG_RAID_TARGET_UPDATE:
|
||||
case Opcode::SMSG_GROUP_SET_LEADER:
|
||||
LOG_DEBUG("Ignoring known opcode: 0x", std::hex, opcode, std::dec);
|
||||
break;
|
||||
|
||||
default:
|
||||
LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec);
|
||||
break;
|
||||
|
|
@ -357,6 +509,9 @@ void GameHandler::selectCharacter(uint64_t characterGuid) {
|
|||
}
|
||||
}
|
||||
|
||||
// Store player GUID
|
||||
playerGuid = characterGuid;
|
||||
|
||||
// Build CMSG_PLAYER_LOGIN packet
|
||||
auto packet = PlayerLoginPacket::build(characterGuid);
|
||||
|
||||
|
|
@ -400,6 +555,13 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) {
|
|||
movementInfo.flags = 0;
|
||||
movementInfo.flags2 = 0;
|
||||
movementInfo.time = 0;
|
||||
|
||||
// Send CMSG_SET_ACTIVE_MOVER (required by some servers)
|
||||
if (playerGuid != 0 && socket) {
|
||||
auto activeMoverPacket = SetActiveMoverPacket::build(playerGuid);
|
||||
socket->send(activeMoverPacket);
|
||||
LOG_INFO("Sent CMSG_SET_ACTIVE_MOVER for player 0x", std::hex, playerGuid, std::dec);
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleAccountDataTimes(network::Packet& packet) {
|
||||
|
|
@ -603,6 +765,35 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
|
||||
// Add to manager
|
||||
entityManager.addEntity(block.guid, entity);
|
||||
|
||||
// Auto-query names (Phase 1)
|
||||
if (block.objectType == ObjectType::PLAYER) {
|
||||
queryPlayerName(block.guid);
|
||||
} else if (block.objectType == ObjectType::UNIT) {
|
||||
// Extract creature entry from fields (UNIT_FIELD_ENTRY = index 54 in 3.3.5a,
|
||||
// but the OBJECT_FIELD_ENTRY is at index 3)
|
||||
auto it = block.fields.find(3); // OBJECT_FIELD_ENTRY
|
||||
if (it != block.fields.end() && it->second != 0) {
|
||||
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||
unit->setEntry(it->second);
|
||||
queryCreatureInfo(it->second, block.guid);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract health/mana/power from fields (Phase 2)
|
||||
if (block.objectType == ObjectType::UNIT || block.objectType == ObjectType::PLAYER) {
|
||||
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||
auto hpIt = block.fields.find(24); // UNIT_FIELD_HEALTH
|
||||
if (hpIt != block.fields.end()) unit->setHealth(hpIt->second);
|
||||
auto maxHpIt = block.fields.find(32); // UNIT_FIELD_MAXHEALTH
|
||||
if (maxHpIt != block.fields.end()) unit->setMaxHealth(maxHpIt->second);
|
||||
auto powerIt = block.fields.find(25); // UNIT_FIELD_POWER1
|
||||
if (powerIt != block.fields.end()) unit->setPower(powerIt->second);
|
||||
auto maxPowerIt = block.fields.find(33); // UNIT_FIELD_MAXPOWER1
|
||||
if (maxPowerIt != block.fields.end()) unit->setMaxPower(maxPowerIt->second);
|
||||
auto levelIt = block.fields.find(54); // UNIT_FIELD_LEVEL
|
||||
if (levelIt != block.fields.end()) unit->setLevel(levelIt->second);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -613,6 +804,22 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
for (const auto& field : block.fields) {
|
||||
entity->setField(field.first, field.second);
|
||||
}
|
||||
|
||||
// Update cached health/mana/power values (Phase 2)
|
||||
if (entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) {
|
||||
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||
auto hpIt = block.fields.find(24);
|
||||
if (hpIt != block.fields.end()) unit->setHealth(hpIt->second);
|
||||
auto maxHpIt = block.fields.find(32);
|
||||
if (maxHpIt != block.fields.end()) unit->setMaxHealth(maxHpIt->second);
|
||||
auto powerIt = block.fields.find(25);
|
||||
if (powerIt != block.fields.end()) unit->setPower(powerIt->second);
|
||||
auto maxPowerIt = block.fields.find(33);
|
||||
if (maxPowerIt != block.fields.end()) unit->setMaxPower(maxPowerIt->second);
|
||||
auto levelIt = block.fields.find(54);
|
||||
if (levelIt != block.fields.end()) unit->setLevel(levelIt->second);
|
||||
}
|
||||
|
||||
LOG_DEBUG("Updated entity fields: 0x", std::hex, block.guid, std::dec);
|
||||
} else {
|
||||
LOG_WARNING("VALUES update for unknown entity: 0x", std::hex, block.guid, std::dec);
|
||||
|
|
@ -737,6 +944,13 @@ void GameHandler::handleMessageChat(network::Packet& packet) {
|
|||
void GameHandler::setTarget(uint64_t guid) {
|
||||
if (guid == targetGuid) return;
|
||||
targetGuid = guid;
|
||||
|
||||
// Inform server of target selection (Phase 1)
|
||||
if (state == WorldState::IN_WORLD && socket) {
|
||||
auto packet = SetSelectionPacket::build(guid);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
if (guid != 0) {
|
||||
LOG_INFO("Target set: 0x", std::hex, guid, std::dec);
|
||||
}
|
||||
|
|
@ -815,6 +1029,539 @@ std::vector<MessageChatData> GameHandler::getChatHistory(size_t maxMessages) con
|
|||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 1: Name Queries
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::queryPlayerName(uint64_t guid) {
|
||||
if (playerNameCache.count(guid) || pendingNameQueries.count(guid)) return;
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
|
||||
pendingNameQueries.insert(guid);
|
||||
auto packet = NameQueryPacket::build(guid);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::queryCreatureInfo(uint32_t entry, uint64_t guid) {
|
||||
if (creatureInfoCache.count(entry) || pendingCreatureQueries.count(entry)) return;
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
|
||||
pendingCreatureQueries.insert(entry);
|
||||
auto packet = CreatureQueryPacket::build(entry, guid);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
std::string GameHandler::getCachedPlayerName(uint64_t guid) const {
|
||||
auto it = playerNameCache.find(guid);
|
||||
return (it != playerNameCache.end()) ? it->second : "";
|
||||
}
|
||||
|
||||
std::string GameHandler::getCachedCreatureName(uint32_t entry) const {
|
||||
auto it = creatureInfoCache.find(entry);
|
||||
return (it != creatureInfoCache.end()) ? it->second.name : "";
|
||||
}
|
||||
|
||||
void GameHandler::handleNameQueryResponse(network::Packet& packet) {
|
||||
NameQueryResponseData data;
|
||||
if (!NameQueryResponseParser::parse(packet, data)) return;
|
||||
|
||||
pendingNameQueries.erase(data.guid);
|
||||
|
||||
if (data.isValid()) {
|
||||
playerNameCache[data.guid] = data.name;
|
||||
// Update entity name
|
||||
auto entity = entityManager.getEntity(data.guid);
|
||||
if (entity && entity->getType() == ObjectType::PLAYER) {
|
||||
auto player = std::static_pointer_cast<Player>(entity);
|
||||
player->setName(data.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleCreatureQueryResponse(network::Packet& packet) {
|
||||
CreatureQueryResponseData data;
|
||||
if (!CreatureQueryResponseParser::parse(packet, data)) return;
|
||||
|
||||
pendingCreatureQueries.erase(data.entry);
|
||||
|
||||
if (data.isValid()) {
|
||||
creatureInfoCache[data.entry] = data;
|
||||
// Update all unit entities with this entry
|
||||
for (auto& [guid, entity] : entityManager.getEntities()) {
|
||||
if (entity->getType() == ObjectType::UNIT) {
|
||||
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||
if (unit->getEntry() == data.entry) {
|
||||
unit->setName(data.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 2: Combat
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::startAutoAttack(uint64_t targetGuid) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
autoAttacking = true;
|
||||
autoAttackTarget = targetGuid;
|
||||
auto packet = AttackSwingPacket::build(targetGuid);
|
||||
socket->send(packet);
|
||||
LOG_INFO("Starting auto-attack on 0x", std::hex, targetGuid, std::dec);
|
||||
}
|
||||
|
||||
void GameHandler::stopAutoAttack() {
|
||||
if (!autoAttacking) return;
|
||||
autoAttacking = false;
|
||||
autoAttackTarget = 0;
|
||||
if (state == WorldState::IN_WORLD && socket) {
|
||||
auto packet = AttackStopPacket::build();
|
||||
socket->send(packet);
|
||||
}
|
||||
LOG_INFO("Stopping auto-attack");
|
||||
}
|
||||
|
||||
void GameHandler::addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId, bool isPlayerSource) {
|
||||
CombatTextEntry entry;
|
||||
entry.type = type;
|
||||
entry.amount = amount;
|
||||
entry.spellId = spellId;
|
||||
entry.age = 0.0f;
|
||||
entry.isPlayerSource = isPlayerSource;
|
||||
combatText.push_back(entry);
|
||||
}
|
||||
|
||||
void GameHandler::updateCombatText(float deltaTime) {
|
||||
for (auto& entry : combatText) {
|
||||
entry.age += deltaTime;
|
||||
}
|
||||
combatText.erase(
|
||||
std::remove_if(combatText.begin(), combatText.end(),
|
||||
[](const CombatTextEntry& e) { return e.isExpired(); }),
|
||||
combatText.end());
|
||||
}
|
||||
|
||||
void GameHandler::handleAttackStart(network::Packet& packet) {
|
||||
AttackStartData data;
|
||||
if (!AttackStartParser::parse(packet, data)) return;
|
||||
|
||||
if (data.attackerGuid == playerGuid) {
|
||||
autoAttacking = true;
|
||||
autoAttackTarget = data.victimGuid;
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleAttackStop(network::Packet& packet) {
|
||||
AttackStopData data;
|
||||
if (!AttackStopParser::parse(packet, data)) return;
|
||||
|
||||
if (data.attackerGuid == playerGuid) {
|
||||
autoAttacking = false;
|
||||
autoAttackTarget = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleAttackerStateUpdate(network::Packet& packet) {
|
||||
AttackerStateUpdateData data;
|
||||
if (!AttackerStateUpdateParser::parse(packet, data)) return;
|
||||
|
||||
bool isPlayerAttacker = (data.attackerGuid == playerGuid);
|
||||
bool isPlayerTarget = (data.targetGuid == playerGuid);
|
||||
|
||||
if (data.isMiss()) {
|
||||
addCombatText(CombatTextEntry::MISS, 0, 0, isPlayerAttacker);
|
||||
} else if (data.victimState == 1) {
|
||||
addCombatText(CombatTextEntry::DODGE, 0, 0, isPlayerAttacker);
|
||||
} else if (data.victimState == 2) {
|
||||
addCombatText(CombatTextEntry::PARRY, 0, 0, isPlayerAttacker);
|
||||
} else {
|
||||
auto type = data.isCrit() ? CombatTextEntry::CRIT_DAMAGE : CombatTextEntry::MELEE_DAMAGE;
|
||||
addCombatText(type, data.totalDamage, 0, isPlayerAttacker);
|
||||
}
|
||||
|
||||
(void)isPlayerTarget; // Used for future incoming damage display
|
||||
}
|
||||
|
||||
void GameHandler::handleSpellDamageLog(network::Packet& packet) {
|
||||
SpellDamageLogData data;
|
||||
if (!SpellDamageLogParser::parse(packet, data)) return;
|
||||
|
||||
bool isPlayerSource = (data.attackerGuid == playerGuid);
|
||||
auto type = data.isCrit ? CombatTextEntry::CRIT_DAMAGE : CombatTextEntry::SPELL_DAMAGE;
|
||||
addCombatText(type, static_cast<int32_t>(data.damage), data.spellId, isPlayerSource);
|
||||
}
|
||||
|
||||
void GameHandler::handleSpellHealLog(network::Packet& packet) {
|
||||
SpellHealLogData data;
|
||||
if (!SpellHealLogParser::parse(packet, data)) return;
|
||||
|
||||
bool isPlayerSource = (data.casterGuid == playerGuid);
|
||||
auto type = data.isCrit ? CombatTextEntry::CRIT_HEAL : CombatTextEntry::HEAL;
|
||||
addCombatText(type, static_cast<int32_t>(data.heal), data.spellId, isPlayerSource);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 3: Spells
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
if (casting) return; // Already casting
|
||||
|
||||
uint64_t target = targetGuid != 0 ? targetGuid : targetGuid;
|
||||
auto packet = CastSpellPacket::build(spellId, target, ++castCount);
|
||||
socket->send(packet);
|
||||
LOG_INFO("Casting spell: ", spellId, " on 0x", std::hex, target, std::dec);
|
||||
}
|
||||
|
||||
void GameHandler::cancelCast() {
|
||||
if (!casting) return;
|
||||
if (state == WorldState::IN_WORLD && socket) {
|
||||
auto packet = CancelCastPacket::build(currentCastSpellId);
|
||||
socket->send(packet);
|
||||
}
|
||||
casting = false;
|
||||
currentCastSpellId = 0;
|
||||
castTimeRemaining = 0.0f;
|
||||
}
|
||||
|
||||
void GameHandler::cancelAura(uint32_t spellId) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = CancelAuraPacket::build(spellId);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::setActionBarSlot(int slot, ActionBarSlot::Type type, uint32_t id) {
|
||||
if (slot < 0 || slot >= ACTION_BAR_SLOTS) return;
|
||||
actionBar[slot].type = type;
|
||||
actionBar[slot].id = id;
|
||||
}
|
||||
|
||||
float GameHandler::getSpellCooldown(uint32_t spellId) const {
|
||||
auto it = spellCooldowns.find(spellId);
|
||||
return (it != spellCooldowns.end()) ? it->second : 0.0f;
|
||||
}
|
||||
|
||||
void GameHandler::handleInitialSpells(network::Packet& packet) {
|
||||
InitialSpellsData data;
|
||||
if (!InitialSpellsParser::parse(packet, data)) return;
|
||||
|
||||
knownSpells = data.spellIds;
|
||||
|
||||
// Set initial cooldowns
|
||||
for (const auto& cd : data.cooldowns) {
|
||||
if (cd.cooldownMs > 0) {
|
||||
spellCooldowns[cd.spellId] = cd.cooldownMs / 1000.0f;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-populate action bar with first 12 spells
|
||||
for (int i = 0; i < ACTION_BAR_SLOTS && i < static_cast<int>(knownSpells.size()); ++i) {
|
||||
actionBar[i].type = ActionBarSlot::SPELL;
|
||||
actionBar[i].id = knownSpells[i];
|
||||
}
|
||||
|
||||
LOG_INFO("Learned ", knownSpells.size(), " spells");
|
||||
}
|
||||
|
||||
void GameHandler::handleCastFailed(network::Packet& packet) {
|
||||
CastFailedData data;
|
||||
if (!CastFailedParser::parse(packet, data)) return;
|
||||
|
||||
casting = false;
|
||||
currentCastSpellId = 0;
|
||||
castTimeRemaining = 0.0f;
|
||||
|
||||
// Add system message about failed cast
|
||||
MessageChatData msg;
|
||||
msg.type = ChatType::SYSTEM;
|
||||
msg.language = ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Spell cast failed (error " + std::to_string(data.result) + ")";
|
||||
addLocalChatMessage(msg);
|
||||
}
|
||||
|
||||
void GameHandler::handleSpellStart(network::Packet& packet) {
|
||||
SpellStartData data;
|
||||
if (!SpellStartParser::parse(packet, data)) return;
|
||||
|
||||
// If this is the player's own cast, start cast bar
|
||||
if (data.casterUnit == playerGuid && data.castTime > 0) {
|
||||
casting = true;
|
||||
currentCastSpellId = data.spellId;
|
||||
castTimeTotal = data.castTime / 1000.0f;
|
||||
castTimeRemaining = castTimeTotal;
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleSpellGo(network::Packet& packet) {
|
||||
SpellGoData data;
|
||||
if (!SpellGoParser::parse(packet, data)) return;
|
||||
|
||||
// Cast completed
|
||||
if (data.casterUnit == playerGuid) {
|
||||
casting = false;
|
||||
currentCastSpellId = 0;
|
||||
castTimeRemaining = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleSpellCooldown(network::Packet& packet) {
|
||||
SpellCooldownData data;
|
||||
if (!SpellCooldownParser::parse(packet, data)) return;
|
||||
|
||||
for (const auto& [spellId, cooldownMs] : data.cooldowns) {
|
||||
float seconds = cooldownMs / 1000.0f;
|
||||
spellCooldowns[spellId] = seconds;
|
||||
// Update action bar cooldowns
|
||||
for (auto& slot : actionBar) {
|
||||
if (slot.type == ActionBarSlot::SPELL && slot.id == spellId) {
|
||||
slot.cooldownTotal = seconds;
|
||||
slot.cooldownRemaining = seconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleCooldownEvent(network::Packet& packet) {
|
||||
uint32_t spellId = packet.readUInt32();
|
||||
// Cooldown finished
|
||||
spellCooldowns.erase(spellId);
|
||||
for (auto& slot : actionBar) {
|
||||
if (slot.type == ActionBarSlot::SPELL && slot.id == spellId) {
|
||||
slot.cooldownRemaining = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleAuraUpdate(network::Packet& packet, bool isAll) {
|
||||
AuraUpdateData data;
|
||||
if (!AuraUpdateParser::parse(packet, data, isAll)) return;
|
||||
|
||||
// Determine which aura list to update
|
||||
std::vector<AuraSlot>* auraList = nullptr;
|
||||
if (data.guid == playerGuid) {
|
||||
auraList = &playerAuras;
|
||||
} else if (data.guid == targetGuid) {
|
||||
auraList = &targetAuras;
|
||||
}
|
||||
|
||||
if (auraList) {
|
||||
for (const auto& [slot, aura] : data.updates) {
|
||||
// Ensure vector is large enough
|
||||
while (auraList->size() <= slot) {
|
||||
auraList->push_back(AuraSlot{});
|
||||
}
|
||||
(*auraList)[slot] = aura;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleLearnedSpell(network::Packet& packet) {
|
||||
uint32_t spellId = packet.readUInt32();
|
||||
knownSpells.push_back(spellId);
|
||||
LOG_INFO("Learned spell: ", spellId);
|
||||
}
|
||||
|
||||
void GameHandler::handleRemovedSpell(network::Packet& packet) {
|
||||
uint32_t spellId = packet.readUInt32();
|
||||
knownSpells.erase(
|
||||
std::remove(knownSpells.begin(), knownSpells.end(), spellId),
|
||||
knownSpells.end());
|
||||
LOG_INFO("Removed spell: ", spellId);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 4: Group/Party
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::inviteToGroup(const std::string& playerName) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = GroupInvitePacket::build(playerName);
|
||||
socket->send(packet);
|
||||
LOG_INFO("Inviting ", playerName, " to group");
|
||||
}
|
||||
|
||||
void GameHandler::acceptGroupInvite() {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
pendingGroupInvite = false;
|
||||
auto packet = GroupAcceptPacket::build();
|
||||
socket->send(packet);
|
||||
LOG_INFO("Accepted group invite");
|
||||
}
|
||||
|
||||
void GameHandler::declineGroupInvite() {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
pendingGroupInvite = false;
|
||||
auto packet = GroupDeclinePacket::build();
|
||||
socket->send(packet);
|
||||
LOG_INFO("Declined group invite");
|
||||
}
|
||||
|
||||
void GameHandler::leaveGroup() {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = GroupDisbandPacket::build();
|
||||
socket->send(packet);
|
||||
partyData = GroupListData{};
|
||||
LOG_INFO("Left group");
|
||||
}
|
||||
|
||||
void GameHandler::handleGroupInvite(network::Packet& packet) {
|
||||
GroupInviteResponseData data;
|
||||
if (!GroupInviteResponseParser::parse(packet, data)) return;
|
||||
|
||||
pendingGroupInvite = true;
|
||||
pendingInviterName = data.inviterName;
|
||||
LOG_INFO("Group invite from: ", data.inviterName);
|
||||
}
|
||||
|
||||
void GameHandler::handleGroupDecline(network::Packet& packet) {
|
||||
GroupDeclineData data;
|
||||
if (!GroupDeclineResponseParser::parse(packet, data)) return;
|
||||
|
||||
MessageChatData msg;
|
||||
msg.type = ChatType::SYSTEM;
|
||||
msg.language = ChatLanguage::UNIVERSAL;
|
||||
msg.message = data.playerName + " has declined your group invitation.";
|
||||
addLocalChatMessage(msg);
|
||||
}
|
||||
|
||||
void GameHandler::handleGroupList(network::Packet& packet) {
|
||||
if (!GroupListParser::parse(packet, partyData)) return;
|
||||
|
||||
if (partyData.isEmpty()) {
|
||||
LOG_INFO("No longer in a group");
|
||||
} else {
|
||||
LOG_INFO("In group with ", partyData.memberCount, " members");
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleGroupUninvite(network::Packet& packet) {
|
||||
(void)packet;
|
||||
partyData = GroupListData{};
|
||||
LOG_INFO("Removed from group");
|
||||
|
||||
MessageChatData msg;
|
||||
msg.type = ChatType::SYSTEM;
|
||||
msg.language = ChatLanguage::UNIVERSAL;
|
||||
msg.message = "You have been removed from the group.";
|
||||
addLocalChatMessage(msg);
|
||||
}
|
||||
|
||||
void GameHandler::handlePartyCommandResult(network::Packet& packet) {
|
||||
PartyCommandResultData data;
|
||||
if (!PartyCommandResultParser::parse(packet, data)) return;
|
||||
|
||||
if (data.result != PartyResult::OK) {
|
||||
MessageChatData msg;
|
||||
msg.type = ChatType::SYSTEM;
|
||||
msg.language = ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Party command failed (error " + std::to_string(static_cast<uint32_t>(data.result)) + ")";
|
||||
if (!data.name.empty()) msg.message += " for " + data.name;
|
||||
addLocalChatMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: Loot, Gossip, Vendor
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::lootTarget(uint64_t guid) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = LootPacket::build(guid);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::lootItem(uint8_t slotIndex) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = AutostoreLootItemPacket::build(slotIndex);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::closeLoot() {
|
||||
if (!lootWindowOpen) return;
|
||||
lootWindowOpen = false;
|
||||
if (state == WorldState::IN_WORLD && socket) {
|
||||
auto packet = LootReleasePacket::build(currentLoot.lootGuid);
|
||||
socket->send(packet);
|
||||
}
|
||||
currentLoot = LootResponseData{};
|
||||
}
|
||||
|
||||
void GameHandler::interactWithNpc(uint64_t guid) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = GossipHelloPacket::build(guid);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::selectGossipOption(uint32_t optionId) {
|
||||
if (state != WorldState::IN_WORLD || !socket || !gossipWindowOpen) return;
|
||||
auto packet = GossipSelectOptionPacket::build(currentGossip.npcGuid, optionId);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::closeGossip() {
|
||||
gossipWindowOpen = false;
|
||||
currentGossip = GossipMessageData{};
|
||||
}
|
||||
|
||||
void GameHandler::openVendor(uint64_t npcGuid) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = ListInventoryPacket::build(npcGuid);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::buyItem(uint64_t vendorGuid, uint32_t itemId, uint32_t slot, uint8_t count) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = BuyItemPacket::build(vendorGuid, itemId, slot, count);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::sellItem(uint64_t vendorGuid, uint64_t itemGuid, uint8_t count) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = SellItemPacket::build(vendorGuid, itemGuid, count);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::handleLootResponse(network::Packet& packet) {
|
||||
if (!LootResponseParser::parse(packet, currentLoot)) return;
|
||||
lootWindowOpen = true;
|
||||
}
|
||||
|
||||
void GameHandler::handleLootReleaseResponse(network::Packet& packet) {
|
||||
(void)packet;
|
||||
lootWindowOpen = false;
|
||||
currentLoot = LootResponseData{};
|
||||
}
|
||||
|
||||
void GameHandler::handleLootRemoved(network::Packet& packet) {
|
||||
uint8_t slotIndex = packet.readUInt8();
|
||||
for (auto it = currentLoot.items.begin(); it != currentLoot.items.end(); ++it) {
|
||||
if (it->slotIndex == slotIndex) {
|
||||
currentLoot.items.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleGossipMessage(network::Packet& packet) {
|
||||
if (!GossipMessageParser::parse(packet, currentGossip)) return;
|
||||
gossipWindowOpen = true;
|
||||
vendorWindowOpen = false; // Close vendor if gossip opens
|
||||
}
|
||||
|
||||
void GameHandler::handleGossipComplete(network::Packet& packet) {
|
||||
(void)packet;
|
||||
gossipWindowOpen = false;
|
||||
currentGossip = GossipMessageData{};
|
||||
}
|
||||
|
||||
void GameHandler::handleListInventory(network::Packet& packet) {
|
||||
if (!ListInventoryParser::parse(packet, currentVendorItems)) return;
|
||||
vendorWindowOpen = true;
|
||||
gossipWindowOpen = false; // Close gossip if vendor opens
|
||||
}
|
||||
|
||||
uint32_t GameHandler::generateClientSeed() {
|
||||
// Generate cryptographically random seed
|
||||
std::random_device rd;
|
||||
|
|
|
|||
|
|
@ -861,5 +861,642 @@ const char* getChatTypeString(ChatType type) {
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 1: Foundation — Targeting, Name Queries
|
||||
// ============================================================
|
||||
|
||||
network::Packet SetSelectionPacket::build(uint64_t targetGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_SET_SELECTION));
|
||||
packet.writeUInt64(targetGuid);
|
||||
LOG_DEBUG("Built CMSG_SET_SELECTION: target=0x", std::hex, targetGuid, std::dec);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet SetActiveMoverPacket::build(uint64_t guid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_SET_ACTIVE_MOVER));
|
||||
packet.writeUInt64(guid);
|
||||
LOG_DEBUG("Built CMSG_SET_ACTIVE_MOVER: guid=0x", std::hex, guid, std::dec);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet NameQueryPacket::build(uint64_t playerGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_NAME_QUERY));
|
||||
packet.writeUInt64(playerGuid);
|
||||
LOG_DEBUG("Built CMSG_NAME_QUERY: guid=0x", std::hex, playerGuid, std::dec);
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool NameQueryResponseParser::parse(network::Packet& packet, NameQueryResponseData& data) {
|
||||
// 3.3.5a: packedGuid, uint8 found
|
||||
// If found==0: CString name, CString realmName, uint8 race, uint8 gender, uint8 classId
|
||||
data.guid = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.found = packet.readUInt8();
|
||||
|
||||
if (data.found != 0) {
|
||||
LOG_DEBUG("Name query: player not found for GUID 0x", std::hex, data.guid, std::dec);
|
||||
return true; // Valid response, just not found
|
||||
}
|
||||
|
||||
data.name = packet.readString();
|
||||
data.realmName = packet.readString();
|
||||
data.race = packet.readUInt8();
|
||||
data.gender = packet.readUInt8();
|
||||
data.classId = packet.readUInt8();
|
||||
|
||||
LOG_INFO("Name query response: ", data.name, " (race=", (int)data.race,
|
||||
" class=", (int)data.classId, ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
network::Packet CreatureQueryPacket::build(uint32_t entry, uint64_t guid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CREATURE_QUERY));
|
||||
packet.writeUInt32(entry);
|
||||
packet.writeUInt64(guid);
|
||||
LOG_DEBUG("Built CMSG_CREATURE_QUERY: entry=", entry, " guid=0x", std::hex, guid, std::dec);
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool CreatureQueryResponseParser::parse(network::Packet& packet, CreatureQueryResponseData& data) {
|
||||
data.entry = packet.readUInt32();
|
||||
|
||||
// High bit set means creature not found
|
||||
if (data.entry & 0x80000000) {
|
||||
data.entry &= ~0x80000000;
|
||||
LOG_DEBUG("Creature query: entry ", data.entry, " not found");
|
||||
data.name = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
// 4 name strings (only first is usually populated)
|
||||
data.name = packet.readString();
|
||||
packet.readString(); // name2
|
||||
packet.readString(); // name3
|
||||
packet.readString(); // name4
|
||||
data.subName = packet.readString();
|
||||
data.iconName = packet.readString();
|
||||
data.typeFlags = packet.readUInt32();
|
||||
data.creatureType = packet.readUInt32();
|
||||
data.family = packet.readUInt32();
|
||||
data.rank = packet.readUInt32();
|
||||
|
||||
// Skip remaining fields (kill credits, display IDs, modifiers, quest items, etc.)
|
||||
// We've got what we need for display purposes
|
||||
|
||||
LOG_INFO("Creature query response: ", data.name, " (type=", data.creatureType,
|
||||
" rank=", data.rank, ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 2: Combat Core
|
||||
// ============================================================
|
||||
|
||||
network::Packet AttackSwingPacket::build(uint64_t targetGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_ATTACKSWING));
|
||||
packet.writeUInt64(targetGuid);
|
||||
LOG_DEBUG("Built CMSG_ATTACKSWING: target=0x", std::hex, targetGuid, std::dec);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet AttackStopPacket::build() {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_ATTACKSTOP));
|
||||
LOG_DEBUG("Built CMSG_ATTACKSTOP");
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool AttackStartParser::parse(network::Packet& packet, AttackStartData& data) {
|
||||
if (packet.getSize() < 16) return false;
|
||||
data.attackerGuid = packet.readUInt64();
|
||||
data.victimGuid = packet.readUInt64();
|
||||
LOG_INFO("Attack started: 0x", std::hex, data.attackerGuid,
|
||||
" -> 0x", data.victimGuid, std::dec);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AttackStopParser::parse(network::Packet& packet, AttackStopData& data) {
|
||||
data.attackerGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.victimGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
if (packet.getReadPos() < packet.getSize()) {
|
||||
data.unknown = packet.readUInt32();
|
||||
}
|
||||
LOG_INFO("Attack stopped: 0x", std::hex, data.attackerGuid, std::dec);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpdateData& data) {
|
||||
data.hitInfo = packet.readUInt32();
|
||||
data.attackerGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.totalDamage = static_cast<int32_t>(packet.readUInt32());
|
||||
data.subDamageCount = packet.readUInt8();
|
||||
|
||||
for (uint8_t i = 0; i < data.subDamageCount; ++i) {
|
||||
SubDamage sub;
|
||||
sub.schoolMask = packet.readUInt32();
|
||||
sub.damage = packet.readFloat();
|
||||
sub.intDamage = packet.readUInt32();
|
||||
sub.absorbed = packet.readUInt32();
|
||||
sub.resisted = packet.readUInt32();
|
||||
data.subDamages.push_back(sub);
|
||||
}
|
||||
|
||||
data.victimState = packet.readUInt32();
|
||||
data.overkill = static_cast<int32_t>(packet.readUInt32());
|
||||
|
||||
// Read blocked amount
|
||||
if (packet.getReadPos() < packet.getSize()) {
|
||||
data.blocked = packet.readUInt32();
|
||||
}
|
||||
|
||||
LOG_INFO("Melee hit: ", data.totalDamage, " damage",
|
||||
data.isCrit() ? " (CRIT)" : "",
|
||||
data.isMiss() ? " (MISS)" : "");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SpellDamageLogParser::parse(network::Packet& packet, SpellDamageLogData& data) {
|
||||
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.attackerGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.spellId = packet.readUInt32();
|
||||
data.damage = packet.readUInt32();
|
||||
data.overkill = packet.readUInt32();
|
||||
data.schoolMask = packet.readUInt8();
|
||||
data.absorbed = packet.readUInt32();
|
||||
data.resisted = packet.readUInt32();
|
||||
|
||||
// Skip remaining fields
|
||||
uint8_t periodicLog = packet.readUInt8();
|
||||
(void)periodicLog;
|
||||
packet.readUInt8(); // unused
|
||||
packet.readUInt32(); // blocked
|
||||
uint32_t flags = packet.readUInt32();
|
||||
(void)flags;
|
||||
// Check crit flag
|
||||
data.isCrit = (flags & 0x02) != 0;
|
||||
|
||||
LOG_INFO("Spell damage: spellId=", data.spellId, " dmg=", data.damage,
|
||||
data.isCrit ? " CRIT" : "");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SpellHealLogParser::parse(network::Packet& packet, SpellHealLogData& data) {
|
||||
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.casterGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.spellId = packet.readUInt32();
|
||||
data.heal = packet.readUInt32();
|
||||
data.overheal = packet.readUInt32();
|
||||
data.absorbed = packet.readUInt32();
|
||||
uint8_t critFlag = packet.readUInt8();
|
||||
data.isCrit = (critFlag != 0);
|
||||
|
||||
LOG_INFO("Spell heal: spellId=", data.spellId, " heal=", data.heal,
|
||||
data.isCrit ? " CRIT" : "");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 3: Spells, Action Bar, Auras
|
||||
// ============================================================
|
||||
|
||||
bool InitialSpellsParser::parse(network::Packet& packet, InitialSpellsData& data) {
|
||||
data.talentSpec = packet.readUInt8();
|
||||
uint16_t spellCount = packet.readUInt16();
|
||||
|
||||
data.spellIds.reserve(spellCount);
|
||||
for (uint16_t i = 0; i < spellCount; ++i) {
|
||||
uint32_t spellId = packet.readUInt32();
|
||||
packet.readUInt16(); // unknown (always 0)
|
||||
if (spellId != 0) {
|
||||
data.spellIds.push_back(spellId);
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t cooldownCount = packet.readUInt16();
|
||||
data.cooldowns.reserve(cooldownCount);
|
||||
for (uint16_t i = 0; i < cooldownCount; ++i) {
|
||||
SpellCooldownEntry entry;
|
||||
entry.spellId = packet.readUInt32();
|
||||
entry.itemId = packet.readUInt16();
|
||||
entry.categoryId = packet.readUInt16();
|
||||
entry.cooldownMs = packet.readUInt32();
|
||||
entry.categoryCooldownMs = packet.readUInt32();
|
||||
data.cooldowns.push_back(entry);
|
||||
}
|
||||
|
||||
LOG_INFO("Initial spells: ", data.spellIds.size(), " spells, ",
|
||||
data.cooldowns.size(), " cooldowns");
|
||||
return true;
|
||||
}
|
||||
|
||||
network::Packet CastSpellPacket::build(uint32_t spellId, uint64_t targetGuid, uint8_t castCount) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CAST_SPELL));
|
||||
packet.writeUInt8(castCount);
|
||||
packet.writeUInt32(spellId);
|
||||
packet.writeUInt8(0x00); // castFlags = 0 for normal cast
|
||||
|
||||
// SpellCastTargets
|
||||
if (targetGuid != 0) {
|
||||
packet.writeUInt32(0x02); // TARGET_FLAG_UNIT
|
||||
|
||||
// Write packed GUID
|
||||
uint8_t mask = 0;
|
||||
uint8_t bytes[8];
|
||||
int byteCount = 0;
|
||||
uint64_t g = targetGuid;
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
uint8_t b = g & 0xFF;
|
||||
if (b != 0) {
|
||||
mask |= (1 << i);
|
||||
bytes[byteCount++] = b;
|
||||
}
|
||||
g >>= 8;
|
||||
}
|
||||
packet.writeUInt8(mask);
|
||||
for (int i = 0; i < byteCount; ++i) {
|
||||
packet.writeUInt8(bytes[i]);
|
||||
}
|
||||
} else {
|
||||
packet.writeUInt32(0x00); // TARGET_FLAG_SELF
|
||||
}
|
||||
|
||||
LOG_DEBUG("Built CMSG_CAST_SPELL: spell=", spellId, " target=0x",
|
||||
std::hex, targetGuid, std::dec);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet CancelCastPacket::build(uint32_t spellId) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CANCEL_CAST));
|
||||
packet.writeUInt32(0); // sequence
|
||||
packet.writeUInt32(spellId);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet CancelAuraPacket::build(uint32_t spellId) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CANCEL_AURA));
|
||||
packet.writeUInt32(spellId);
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool CastFailedParser::parse(network::Packet& packet, CastFailedData& data) {
|
||||
data.castCount = packet.readUInt8();
|
||||
data.spellId = packet.readUInt32();
|
||||
data.result = packet.readUInt8();
|
||||
LOG_INFO("Cast failed: spell=", data.spellId, " result=", (int)data.result);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) {
|
||||
data.casterGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.casterUnit = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.castCount = packet.readUInt8();
|
||||
data.spellId = packet.readUInt32();
|
||||
data.castFlags = packet.readUInt32();
|
||||
data.castTime = packet.readUInt32();
|
||||
|
||||
// Read target flags and target (simplified)
|
||||
if (packet.getReadPos() < packet.getSize()) {
|
||||
uint32_t targetFlags = packet.readUInt32();
|
||||
if (targetFlags & 0x02) { // TARGET_FLAG_UNIT
|
||||
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) {
|
||||
data.casterGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.casterUnit = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.castCount = packet.readUInt8();
|
||||
data.spellId = packet.readUInt32();
|
||||
data.castFlags = packet.readUInt32();
|
||||
// Timestamp in 3.3.5a
|
||||
packet.readUInt32();
|
||||
|
||||
data.hitCount = packet.readUInt8();
|
||||
data.hitTargets.reserve(data.hitCount);
|
||||
for (uint8_t i = 0; i < data.hitCount; ++i) {
|
||||
data.hitTargets.push_back(packet.readUInt64());
|
||||
}
|
||||
|
||||
data.missCount = packet.readUInt8();
|
||||
// Skip miss details for now
|
||||
|
||||
LOG_INFO("Spell go: spell=", data.spellId, " hits=", (int)data.hitCount,
|
||||
" misses=", (int)data.missCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AuraUpdateParser::parse(network::Packet& packet, AuraUpdateData& data, bool isAll) {
|
||||
data.guid = UpdateObjectParser::readPackedGuid(packet);
|
||||
|
||||
while (packet.getReadPos() < packet.getSize()) {
|
||||
uint8_t slot = packet.readUInt8();
|
||||
uint32_t spellId = packet.readUInt32();
|
||||
|
||||
AuraSlot aura;
|
||||
if (spellId != 0) {
|
||||
aura.spellId = spellId;
|
||||
aura.flags = packet.readUInt8();
|
||||
aura.level = packet.readUInt8();
|
||||
aura.charges = packet.readUInt8();
|
||||
|
||||
if (!(aura.flags & 0x08)) { // NOT_CASTER flag
|
||||
aura.casterGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
}
|
||||
|
||||
if (aura.flags & 0x20) { // DURATION
|
||||
aura.maxDurationMs = static_cast<int32_t>(packet.readUInt32());
|
||||
aura.durationMs = static_cast<int32_t>(packet.readUInt32());
|
||||
}
|
||||
|
||||
if (aura.flags & 0x40) { // EFFECT_AMOUNTS - skip
|
||||
// 3 effect amounts
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (packet.getReadPos() < packet.getSize()) {
|
||||
packet.readUInt32();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data.updates.push_back({slot, aura});
|
||||
|
||||
// For single update, only one entry
|
||||
if (!isAll) break;
|
||||
}
|
||||
|
||||
LOG_DEBUG("Aura update for 0x", std::hex, data.guid, std::dec,
|
||||
": ", data.updates.size(), " slots");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SpellCooldownParser::parse(network::Packet& packet, SpellCooldownData& data) {
|
||||
data.guid = packet.readUInt64();
|
||||
data.flags = packet.readUInt8();
|
||||
|
||||
while (packet.getReadPos() + 8 <= packet.getSize()) {
|
||||
uint32_t spellId = packet.readUInt32();
|
||||
uint32_t cooldownMs = packet.readUInt32();
|
||||
data.cooldowns.push_back({spellId, cooldownMs});
|
||||
}
|
||||
|
||||
LOG_DEBUG("Spell cooldowns: ", data.cooldowns.size(), " entries");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 4: Group/Party System
|
||||
// ============================================================
|
||||
|
||||
network::Packet GroupInvitePacket::build(const std::string& playerName) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GROUP_INVITE));
|
||||
packet.writeString(playerName);
|
||||
packet.writeUInt32(0); // unused
|
||||
LOG_DEBUG("Built CMSG_GROUP_INVITE: ", playerName);
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool GroupInviteResponseParser::parse(network::Packet& packet, GroupInviteResponseData& data) {
|
||||
data.canAccept = packet.readUInt8();
|
||||
data.inviterName = packet.readString();
|
||||
LOG_INFO("Group invite from: ", data.inviterName, " (canAccept=", (int)data.canAccept, ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
network::Packet GroupAcceptPacket::build() {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GROUP_ACCEPT));
|
||||
packet.writeUInt32(0); // unused in 3.3.5a
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet GroupDeclinePacket::build() {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GROUP_DECLINE));
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet GroupDisbandPacket::build() {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GROUP_DISBAND));
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool GroupListParser::parse(network::Packet& packet, GroupListData& data) {
|
||||
data.groupType = packet.readUInt8();
|
||||
data.subGroup = packet.readUInt8();
|
||||
data.flags = packet.readUInt8();
|
||||
data.roles = packet.readUInt8();
|
||||
|
||||
// Skip LFG data if present
|
||||
if (data.groupType & 0x04) {
|
||||
packet.readUInt8(); // lfg state
|
||||
packet.readUInt32(); // lfg entry
|
||||
packet.readUInt8(); // lfg flags (3.3.5a may not have this)
|
||||
}
|
||||
|
||||
packet.readUInt64(); // group GUID
|
||||
packet.readUInt32(); // counter
|
||||
|
||||
data.memberCount = packet.readUInt32();
|
||||
data.members.reserve(data.memberCount);
|
||||
|
||||
for (uint32_t i = 0; i < data.memberCount; ++i) {
|
||||
GroupMember member;
|
||||
member.name = packet.readString();
|
||||
member.guid = packet.readUInt64();
|
||||
member.isOnline = packet.readUInt8();
|
||||
member.subGroup = packet.readUInt8();
|
||||
member.flags = packet.readUInt8();
|
||||
member.roles = packet.readUInt8();
|
||||
data.members.push_back(member);
|
||||
}
|
||||
|
||||
data.leaderGuid = packet.readUInt64();
|
||||
|
||||
if (data.memberCount > 0 && packet.getReadPos() < packet.getSize()) {
|
||||
data.lootMethod = packet.readUInt8();
|
||||
data.looterGuid = packet.readUInt64();
|
||||
data.lootThreshold = packet.readUInt8();
|
||||
data.difficultyId = packet.readUInt8();
|
||||
data.raidDifficultyId = packet.readUInt8();
|
||||
if (packet.getReadPos() < packet.getSize()) {
|
||||
packet.readUInt8(); // unknown byte
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Group list: ", data.memberCount, " members, leader=0x",
|
||||
std::hex, data.leaderGuid, std::dec);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PartyCommandResultParser::parse(network::Packet& packet, PartyCommandResultData& data) {
|
||||
data.command = static_cast<PartyCommand>(packet.readUInt32());
|
||||
data.name = packet.readString();
|
||||
data.result = static_cast<PartyResult>(packet.readUInt32());
|
||||
LOG_INFO("Party command result: ", (int)data.result);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GroupDeclineResponseParser::parse(network::Packet& packet, GroupDeclineData& data) {
|
||||
data.playerName = packet.readString();
|
||||
LOG_INFO("Group decline from: ", data.playerName);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: Loot System
|
||||
// ============================================================
|
||||
|
||||
network::Packet LootPacket::build(uint64_t targetGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LOOT));
|
||||
packet.writeUInt64(targetGuid);
|
||||
LOG_DEBUG("Built CMSG_LOOT: target=0x", std::hex, targetGuid, std::dec);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet AutostoreLootItemPacket::build(uint8_t slotIndex) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_AUTOSTORE_LOOT_ITEM));
|
||||
packet.writeUInt8(slotIndex);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet LootReleasePacket::build(uint64_t lootGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LOOT_RELEASE));
|
||||
packet.writeUInt64(lootGuid);
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data) {
|
||||
data.lootGuid = packet.readUInt64();
|
||||
data.lootType = packet.readUInt8();
|
||||
data.gold = packet.readUInt32();
|
||||
uint8_t itemCount = packet.readUInt8();
|
||||
|
||||
data.items.reserve(itemCount);
|
||||
for (uint8_t i = 0; i < itemCount; ++i) {
|
||||
LootItem item;
|
||||
item.slotIndex = packet.readUInt8();
|
||||
item.itemId = packet.readUInt32();
|
||||
item.count = packet.readUInt32();
|
||||
item.displayInfoId = packet.readUInt32();
|
||||
item.randomSuffix = packet.readUInt32();
|
||||
item.randomPropertyId = packet.readUInt32();
|
||||
item.lootSlotType = packet.readUInt8();
|
||||
data.items.push_back(item);
|
||||
}
|
||||
|
||||
LOG_INFO("Loot response: ", (int)itemCount, " items, ", data.gold, " copper");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: NPC Gossip
|
||||
// ============================================================
|
||||
|
||||
network::Packet GossipHelloPacket::build(uint64_t npcGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GOSSIP_HELLO));
|
||||
packet.writeUInt64(npcGuid);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet GossipSelectOptionPacket::build(uint64_t npcGuid, uint32_t optionId, const std::string& code) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GOSSIP_SELECT_OPTION));
|
||||
packet.writeUInt64(npcGuid);
|
||||
packet.writeUInt32(optionId);
|
||||
if (!code.empty()) {
|
||||
packet.writeString(code);
|
||||
}
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool GossipMessageParser::parse(network::Packet& packet, GossipMessageData& data) {
|
||||
data.npcGuid = packet.readUInt64();
|
||||
data.menuId = packet.readUInt32();
|
||||
data.titleTextId = packet.readUInt32();
|
||||
uint32_t optionCount = packet.readUInt32();
|
||||
|
||||
data.options.reserve(optionCount);
|
||||
for (uint32_t i = 0; i < optionCount; ++i) {
|
||||
GossipOption opt;
|
||||
opt.id = packet.readUInt32();
|
||||
opt.icon = packet.readUInt8();
|
||||
opt.isCoded = (packet.readUInt8() != 0);
|
||||
opt.boxMoney = packet.readUInt32();
|
||||
opt.text = packet.readString();
|
||||
opt.boxText = packet.readString();
|
||||
data.options.push_back(opt);
|
||||
}
|
||||
|
||||
uint32_t questCount = packet.readUInt32();
|
||||
data.quests.reserve(questCount);
|
||||
for (uint32_t i = 0; i < questCount; ++i) {
|
||||
GossipQuestItem quest;
|
||||
quest.questId = packet.readUInt32();
|
||||
quest.questIcon = packet.readUInt32();
|
||||
quest.questLevel = static_cast<int32_t>(packet.readUInt32());
|
||||
quest.questFlags = packet.readUInt32();
|
||||
quest.isRepeatable = packet.readUInt8();
|
||||
quest.title = packet.readString();
|
||||
data.quests.push_back(quest);
|
||||
}
|
||||
|
||||
LOG_INFO("Gossip: ", optionCount, " options, ", questCount, " quests");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: Vendor
|
||||
// ============================================================
|
||||
|
||||
network::Packet ListInventoryPacket::build(uint64_t npcGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LIST_INVENTORY));
|
||||
packet.writeUInt64(npcGuid);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet BuyItemPacket::build(uint64_t vendorGuid, uint32_t itemId, uint32_t slot, uint8_t count) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_BUY_ITEM));
|
||||
packet.writeUInt64(vendorGuid);
|
||||
packet.writeUInt32(itemId);
|
||||
packet.writeUInt32(slot);
|
||||
packet.writeUInt8(count);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet SellItemPacket::build(uint64_t vendorGuid, uint64_t itemGuid, uint8_t count) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_SELL_ITEM));
|
||||
packet.writeUInt64(vendorGuid);
|
||||
packet.writeUInt64(itemGuid);
|
||||
packet.writeUInt8(count);
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool ListInventoryParser::parse(network::Packet& packet, ListInventoryData& data) {
|
||||
data.vendorGuid = packet.readUInt64();
|
||||
uint8_t itemCount = packet.readUInt8();
|
||||
|
||||
if (itemCount == 0) {
|
||||
LOG_INFO("Vendor has nothing for sale");
|
||||
return true;
|
||||
}
|
||||
|
||||
data.items.reserve(itemCount);
|
||||
for (uint8_t i = 0; i < itemCount; ++i) {
|
||||
VendorItem item;
|
||||
item.slot = packet.readUInt32();
|
||||
item.itemId = packet.readUInt32();
|
||||
item.displayInfoId = packet.readUInt32();
|
||||
item.maxCount = static_cast<int32_t>(packet.readUInt32());
|
||||
item.buyPrice = packet.readUInt32();
|
||||
item.durability = packet.readUInt32();
|
||||
item.stackCount = packet.readUInt32();
|
||||
item.extendedCost = packet.readUInt32();
|
||||
data.items.push_back(item);
|
||||
}
|
||||
|
||||
LOG_INFO("Vendor inventory: ", (int)itemCount, " items");
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -54,21 +54,6 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
// Process targeting input before UI windows
|
||||
processTargetInput(gameHandler);
|
||||
|
||||
// Main menu bar
|
||||
if (ImGui::BeginMainMenuBar()) {
|
||||
if (ImGui::BeginMenu("View")) {
|
||||
ImGui::MenuItem("Player Info", nullptr, &showPlayerInfo);
|
||||
ImGui::MenuItem("Entity List", nullptr, &showEntityWindow);
|
||||
ImGui::MenuItem("Chat", nullptr, &showChatWindow);
|
||||
bool invOpen = inventoryScreen.isOpen();
|
||||
if (ImGui::MenuItem("Inventory", "B", &invOpen)) {
|
||||
inventoryScreen.setOpen(invOpen);
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
ImGui::EndMainMenuBar();
|
||||
}
|
||||
|
||||
// Player unit frame (top-left)
|
||||
renderPlayerFrame(gameHandler);
|
||||
|
||||
|
|
@ -90,6 +75,17 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
renderChatWindow(gameHandler);
|
||||
}
|
||||
|
||||
// ---- New UI elements ----
|
||||
renderActionBar(gameHandler);
|
||||
renderCastBar(gameHandler);
|
||||
renderCombatText(gameHandler);
|
||||
renderPartyFrames(gameHandler);
|
||||
renderGroupInvitePopup(gameHandler);
|
||||
renderBuffBar(gameHandler);
|
||||
renderLootWindow(gameHandler);
|
||||
renderGossipWindow(gameHandler);
|
||||
renderVendorWindow(gameHandler);
|
||||
|
||||
// Inventory (B key toggle handled inside)
|
||||
inventoryScreen.render(gameHandler.getInventory());
|
||||
|
||||
|
|
@ -346,7 +342,40 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
}
|
||||
|
||||
if (input.isKeyJustPressed(SDL_SCANCODE_ESCAPE)) {
|
||||
gameHandler.clearTarget();
|
||||
if (gameHandler.isCasting()) {
|
||||
gameHandler.cancelCast();
|
||||
} else if (gameHandler.isLootWindowOpen()) {
|
||||
gameHandler.closeLoot();
|
||||
} else if (gameHandler.isGossipWindowOpen()) {
|
||||
gameHandler.closeGossip();
|
||||
} else {
|
||||
gameHandler.clearTarget();
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-attack (T key)
|
||||
if (input.isKeyJustPressed(SDL_SCANCODE_T)) {
|
||||
if (gameHandler.hasTarget() && !gameHandler.isAutoAttacking()) {
|
||||
gameHandler.startAutoAttack(gameHandler.getTargetGuid());
|
||||
} else if (gameHandler.isAutoAttacking()) {
|
||||
gameHandler.stopAutoAttack();
|
||||
}
|
||||
}
|
||||
|
||||
// Action bar keys (1-9, 0, -, =)
|
||||
static const SDL_Scancode actionBarKeys[] = {
|
||||
SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_3, SDL_SCANCODE_4,
|
||||
SDL_SCANCODE_5, SDL_SCANCODE_6, SDL_SCANCODE_7, SDL_SCANCODE_8,
|
||||
SDL_SCANCODE_9, SDL_SCANCODE_0, SDL_SCANCODE_MINUS, SDL_SCANCODE_EQUALS
|
||||
};
|
||||
for (int i = 0; i < 12; ++i) {
|
||||
if (input.isKeyJustPressed(actionBarKeys[i])) {
|
||||
const auto& bar = gameHandler.getActionBar();
|
||||
if (bar[i].type == game::ActionBarSlot::SPELL && bar[i].isReady()) {
|
||||
uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
|
||||
gameHandler.castSpell(bar[i].id, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -389,6 +418,27 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
// Don't clear on miss — left-click is also used for camera orbit
|
||||
}
|
||||
}
|
||||
|
||||
// Right-click on target for NPC interaction / loot / auto-attack
|
||||
if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_RIGHT)) {
|
||||
if (gameHandler.hasTarget()) {
|
||||
auto target = gameHandler.getTarget();
|
||||
if (target) {
|
||||
if (target->getType() == game::ObjectType::UNIT) {
|
||||
// Check if unit is dead (health == 0) → loot, otherwise interact/attack
|
||||
auto unit = std::static_pointer_cast<game::Unit>(target);
|
||||
if (unit->getHealth() == 0 && unit->getMaxHealth() > 0) {
|
||||
gameHandler.lootTarget(target->getGuid());
|
||||
} else {
|
||||
// Try NPC interaction first (gossip), fall back to attack
|
||||
gameHandler.interactWithNpc(target->getGuid());
|
||||
}
|
||||
} else if (target->getType() == game::ObjectType::PLAYER) {
|
||||
// Right-click another player could start attack in PvP context
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) {
|
||||
|
|
@ -426,6 +476,16 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) {
|
|||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("Lv %u", playerLevel);
|
||||
|
||||
// Try to get real HP/mana from the player entity
|
||||
auto playerEntity = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid());
|
||||
if (playerEntity && (playerEntity->getType() == game::ObjectType::PLAYER || playerEntity->getType() == game::ObjectType::UNIT)) {
|
||||
auto unit = std::static_pointer_cast<game::Unit>(playerEntity);
|
||||
if (unit->getMaxHealth() > 0) {
|
||||
playerHp = unit->getHealth();
|
||||
playerMaxHp = unit->getMaxHealth();
|
||||
}
|
||||
}
|
||||
|
||||
// Health bar
|
||||
float pct = static_cast<float>(playerHp) / static_cast<float>(playerMaxHp);
|
||||
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.2f, 0.8f, 0.2f, 1.0f));
|
||||
|
|
@ -433,6 +493,29 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) {
|
|||
snprintf(overlay, sizeof(overlay), "%u / %u", playerHp, playerMaxHp);
|
||||
ImGui::ProgressBar(pct, ImVec2(-1, 18), overlay);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// Mana/Power bar (Phase 2)
|
||||
if (playerEntity && (playerEntity->getType() == game::ObjectType::PLAYER || playerEntity->getType() == game::ObjectType::UNIT)) {
|
||||
auto unit = std::static_pointer_cast<game::Unit>(playerEntity);
|
||||
uint32_t power = unit->getPower();
|
||||
uint32_t maxPower = unit->getMaxPower();
|
||||
if (maxPower > 0) {
|
||||
float mpPct = static_cast<float>(power) / static_cast<float>(maxPower);
|
||||
// Color by power type
|
||||
ImVec4 powerColor;
|
||||
switch (unit->getPowerType()) {
|
||||
case 0: powerColor = ImVec4(0.2f, 0.2f, 0.9f, 1.0f); break; // Mana (blue)
|
||||
case 1: powerColor = ImVec4(0.9f, 0.2f, 0.2f, 1.0f); break; // Rage (red)
|
||||
case 3: powerColor = ImVec4(0.9f, 0.9f, 0.2f, 1.0f); break; // Energy (yellow)
|
||||
default: powerColor = ImVec4(0.2f, 0.2f, 0.9f, 1.0f); break;
|
||||
}
|
||||
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, powerColor);
|
||||
char mpOverlay[64];
|
||||
snprintf(mpOverlay, sizeof(mpOverlay), "%u / %u", power, maxPower);
|
||||
ImGui::ProgressBar(mpPct, ImVec2(-1, 14), mpOverlay);
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
|
|
@ -500,6 +583,17 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) {
|
|||
snprintf(overlay, sizeof(overlay), "%u / %u", hp, maxHp);
|
||||
ImGui::ProgressBar(pct, ImVec2(-1, 18), overlay);
|
||||
ImGui::PopStyleColor();
|
||||
// Target mana bar
|
||||
uint32_t targetPower = unit->getPower();
|
||||
uint32_t targetMaxPower = unit->getMaxPower();
|
||||
if (targetMaxPower > 0) {
|
||||
float mpPct = static_cast<float>(targetPower) / static_cast<float>(targetMaxPower);
|
||||
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.2f, 0.2f, 0.9f, 1.0f));
|
||||
char mpOverlay[64];
|
||||
snprintf(mpOverlay, sizeof(mpOverlay), "%u / %u", targetPower, targetMaxPower);
|
||||
ImGui::ProgressBar(mpPct, ImVec2(-1, 14), mpOverlay);
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
} else {
|
||||
ImGui::TextDisabled("No health data");
|
||||
}
|
||||
|
|
@ -561,6 +655,14 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
|
|||
chatInputBuffer[0] = '\0';
|
||||
return;
|
||||
}
|
||||
// /invite command (Phase 4)
|
||||
if (command.size() > 7 && command.substr(0, 7) == "invite ") {
|
||||
std::string targetName = input.substr(8);
|
||||
gameHandler.inviteToGroup(targetName);
|
||||
chatInputBuffer[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
// Not a recognized emote — fall through and send as normal chat
|
||||
}
|
||||
|
||||
|
|
@ -858,4 +960,548 @@ void GameScreen::updateCharacterTextures(game::Inventory& inventory) {
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Action Bar (Phase 3)
|
||||
// ============================================================
|
||||
|
||||
void GameScreen::renderActionBar(game::GameHandler& gameHandler) {
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
||||
|
||||
float slotSize = 48.0f;
|
||||
float spacing = 4.0f;
|
||||
float padding = 8.0f;
|
||||
float barW = 12 * slotSize + 11 * spacing + padding * 2;
|
||||
float barH = slotSize + 24.0f;
|
||||
float barX = (screenW - barW) / 2.0f;
|
||||
float barY = screenH - barH;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(barX, barY), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(barW, barH), ImGuiCond_Always);
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
||||
ImGuiWindowFlags_NoScrollbar;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.05f, 0.05f, 0.05f, 0.9f));
|
||||
|
||||
if (ImGui::Begin("##ActionBar", nullptr, flags)) {
|
||||
const auto& bar = gameHandler.getActionBar();
|
||||
static const char* keyLabels[] = {"1","2","3","4","5","6","7","8","9","0","-","="};
|
||||
|
||||
for (int i = 0; i < 12; ++i) {
|
||||
if (i > 0) ImGui::SameLine(0, spacing);
|
||||
|
||||
ImGui::BeginGroup();
|
||||
ImGui::PushID(i);
|
||||
|
||||
const auto& slot = bar[i];
|
||||
bool onCooldown = !slot.isReady();
|
||||
|
||||
if (onCooldown) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.2f, 0.2f, 0.8f));
|
||||
} else if (slot.isEmpty()) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.15f, 0.15f, 0.8f));
|
||||
} else {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.3f, 0.5f, 0.9f));
|
||||
}
|
||||
|
||||
char label[32];
|
||||
if (slot.type == game::ActionBarSlot::SPELL) {
|
||||
snprintf(label, sizeof(label), "S%u", slot.id);
|
||||
} else {
|
||||
snprintf(label, sizeof(label), "--");
|
||||
}
|
||||
|
||||
if (ImGui::Button(label, ImVec2(slotSize, slotSize))) {
|
||||
if (slot.type == game::ActionBarSlot::SPELL && slot.isReady()) {
|
||||
uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
|
||||
gameHandler.castSpell(slot.id, target);
|
||||
}
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// Cooldown overlay text
|
||||
if (onCooldown) {
|
||||
char cdText[16];
|
||||
snprintf(cdText, sizeof(cdText), "%.0f", slot.cooldownRemaining);
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() - slotSize / 2 - 8);
|
||||
ImGui::TextColored(ImVec4(1, 1, 0, 1), "%s", cdText);
|
||||
}
|
||||
|
||||
// Key label below
|
||||
ImGui::TextDisabled("%s", keyLabels[i]);
|
||||
|
||||
ImGui::PopID();
|
||||
ImGui::EndGroup();
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Cast Bar (Phase 3)
|
||||
// ============================================================
|
||||
|
||||
void GameScreen::renderCastBar(game::GameHandler& gameHandler) {
|
||||
if (!gameHandler.isCasting()) return;
|
||||
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
||||
|
||||
float barW = 300.0f;
|
||||
float barX = (screenW - barW) / 2.0f;
|
||||
float barY = screenH - 120.0f;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(barX, barY), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(barW, 40), ImGuiCond_Always);
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
||||
ImGuiWindowFlags_NoScrollbar;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.9f));
|
||||
|
||||
if (ImGui::Begin("##CastBar", nullptr, flags)) {
|
||||
float progress = gameHandler.getCastProgress();
|
||||
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.8f, 0.6f, 0.2f, 1.0f));
|
||||
|
||||
char overlay[64];
|
||||
snprintf(overlay, sizeof(overlay), "Spell %u (%.1fs)",
|
||||
gameHandler.getCurrentCastSpellId(), gameHandler.getCastTimeRemaining());
|
||||
ImGui::ProgressBar(progress, ImVec2(-1, 20), overlay);
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Floating Combat Text (Phase 2)
|
||||
// ============================================================
|
||||
|
||||
void GameScreen::renderCombatText(game::GameHandler& gameHandler) {
|
||||
const auto& entries = gameHandler.getCombatText();
|
||||
if (entries.empty()) return;
|
||||
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||
|
||||
// Render combat text entries overlaid on screen
|
||||
ImGui::SetNextWindowPos(ImVec2(0, 0));
|
||||
ImGui::SetNextWindowSize(ImVec2(screenW, 400));
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration |
|
||||
ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav;
|
||||
|
||||
if (ImGui::Begin("##CombatText", nullptr, flags)) {
|
||||
float centerX = screenW / 2.0f;
|
||||
int index = 0;
|
||||
for (const auto& entry : entries) {
|
||||
float alpha = 1.0f - (entry.age / game::CombatTextEntry::LIFETIME);
|
||||
float yOffset = 200.0f - entry.age * 60.0f;
|
||||
|
||||
ImVec4 color;
|
||||
char text[64];
|
||||
switch (entry.type) {
|
||||
case game::CombatTextEntry::MELEE_DAMAGE:
|
||||
case game::CombatTextEntry::SPELL_DAMAGE:
|
||||
snprintf(text, sizeof(text), "-%d", entry.amount);
|
||||
color = entry.isPlayerSource ?
|
||||
ImVec4(1.0f, 1.0f, 0.3f, alpha) : // Outgoing = yellow
|
||||
ImVec4(1.0f, 0.3f, 0.3f, alpha); // Incoming = red
|
||||
break;
|
||||
case game::CombatTextEntry::CRIT_DAMAGE:
|
||||
snprintf(text, sizeof(text), "-%d!", entry.amount);
|
||||
color = ImVec4(1.0f, 0.5f, 0.0f, alpha); // Orange for crit
|
||||
break;
|
||||
case game::CombatTextEntry::HEAL:
|
||||
snprintf(text, sizeof(text), "+%d", entry.amount);
|
||||
color = ImVec4(0.3f, 1.0f, 0.3f, alpha);
|
||||
break;
|
||||
case game::CombatTextEntry::CRIT_HEAL:
|
||||
snprintf(text, sizeof(text), "+%d!", entry.amount);
|
||||
color = ImVec4(0.3f, 1.0f, 0.3f, alpha);
|
||||
break;
|
||||
case game::CombatTextEntry::MISS:
|
||||
snprintf(text, sizeof(text), "Miss");
|
||||
color = ImVec4(0.7f, 0.7f, 0.7f, alpha);
|
||||
break;
|
||||
case game::CombatTextEntry::DODGE:
|
||||
snprintf(text, sizeof(text), "Dodge");
|
||||
color = ImVec4(0.7f, 0.7f, 0.7f, alpha);
|
||||
break;
|
||||
case game::CombatTextEntry::PARRY:
|
||||
snprintf(text, sizeof(text), "Parry");
|
||||
color = ImVec4(0.7f, 0.7f, 0.7f, alpha);
|
||||
break;
|
||||
default:
|
||||
snprintf(text, sizeof(text), "%d", entry.amount);
|
||||
color = ImVec4(1.0f, 1.0f, 1.0f, alpha);
|
||||
break;
|
||||
}
|
||||
|
||||
// Stagger entries horizontally
|
||||
float xOffset = centerX + (index % 3 - 1) * 80.0f;
|
||||
ImGui::SetCursorPos(ImVec2(xOffset, yOffset));
|
||||
ImGui::TextColored(color, "%s", text);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Party Frames (Phase 4)
|
||||
// ============================================================
|
||||
|
||||
void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) {
|
||||
if (!gameHandler.isInGroup()) return;
|
||||
|
||||
const auto& partyData = gameHandler.getPartyData();
|
||||
float frameY = 120.0f;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(10.0f, frameY), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(200.0f, 0.0f), ImGuiCond_Always);
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
||||
ImGuiWindowFlags_AlwaysAutoResize;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.8f));
|
||||
|
||||
if (ImGui::Begin("##PartyFrames", nullptr, flags)) {
|
||||
for (const auto& member : partyData.members) {
|
||||
ImGui::PushID(static_cast<int>(member.guid));
|
||||
|
||||
ImVec4 nameColor = member.isOnline ?
|
||||
ImVec4(0.3f, 0.8f, 1.0f, 1.0f) :
|
||||
ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
|
||||
|
||||
// Clickable name to target
|
||||
if (ImGui::Selectable(member.name.c_str(), gameHandler.getTargetGuid() == member.guid)) {
|
||||
gameHandler.setTarget(member.guid);
|
||||
}
|
||||
|
||||
// Try to show health from entity
|
||||
auto entity = gameHandler.getEntityManager().getEntity(member.guid);
|
||||
if (entity && (entity->getType() == game::ObjectType::PLAYER || entity->getType() == game::ObjectType::UNIT)) {
|
||||
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
||||
uint32_t hp = unit->getHealth();
|
||||
uint32_t maxHp = unit->getMaxHealth();
|
||||
if (maxHp > 0) {
|
||||
float pct = static_cast<float>(hp) / static_cast<float>(maxHp);
|
||||
ImGui::PushStyleColor(ImGuiCol_PlotHistogram,
|
||||
pct > 0.5f ? ImVec4(0.2f, 0.8f, 0.2f, 1.0f) :
|
||||
pct > 0.2f ? ImVec4(0.8f, 0.8f, 0.2f, 1.0f) :
|
||||
ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
|
||||
ImGui::ProgressBar(pct, ImVec2(-1, 12), "");
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Group Invite Popup (Phase 4)
|
||||
// ============================================================
|
||||
|
||||
void GameScreen::renderGroupInvitePopup(game::GameHandler& gameHandler) {
|
||||
if (!gameHandler.hasPendingGroupInvite()) return;
|
||||
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 200), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Always);
|
||||
|
||||
if (ImGui::Begin("Group Invite", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) {
|
||||
ImGui::Text("%s has invited you to a group.", gameHandler.getPendingInviterName().c_str());
|
||||
ImGui::Spacing();
|
||||
|
||||
if (ImGui::Button("Accept", ImVec2(130, 30))) {
|
||||
gameHandler.acceptGroupInvite();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Decline", ImVec2(130, 30))) {
|
||||
gameHandler.declineGroupInvite();
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Buff/Debuff Bar (Phase 3)
|
||||
// ============================================================
|
||||
|
||||
void GameScreen::renderBuffBar(game::GameHandler& gameHandler) {
|
||||
const auto& auras = gameHandler.getPlayerAuras();
|
||||
if (auras.empty()) return;
|
||||
|
||||
// Count non-empty auras
|
||||
int activeCount = 0;
|
||||
for (const auto& a : auras) {
|
||||
if (!a.isEmpty()) activeCount++;
|
||||
}
|
||||
if (activeCount == 0) return;
|
||||
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(screenW - 400, 30), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(390, 0), ImGuiCond_Always);
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
||||
ImGuiWindowFlags_AlwaysAutoResize;
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
|
||||
|
||||
if (ImGui::Begin("##BuffBar", nullptr, flags)) {
|
||||
int shown = 0;
|
||||
for (size_t i = 0; i < auras.size() && shown < 16; ++i) {
|
||||
const auto& aura = auras[i];
|
||||
if (aura.isEmpty()) continue;
|
||||
|
||||
if (shown > 0 && shown % 8 != 0) ImGui::SameLine();
|
||||
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
|
||||
// Green border for buffs, red for debuffs
|
||||
bool isBuff = (aura.flags & 0x02) != 0; // POSITIVE flag
|
||||
ImVec4 borderColor = isBuff ? ImVec4(0.2f, 0.8f, 0.2f, 1.0f) : ImVec4(0.8f, 0.2f, 0.2f, 1.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, borderColor);
|
||||
|
||||
char label[16];
|
||||
snprintf(label, sizeof(label), "%u", aura.spellId);
|
||||
if (ImGui::Button(label, ImVec2(40, 40))) {
|
||||
// Right-click to cancel own buffs
|
||||
if (isBuff) {
|
||||
gameHandler.cancelAura(aura.spellId);
|
||||
}
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// Duration text
|
||||
if (aura.durationMs > 0) {
|
||||
int seconds = aura.durationMs / 1000;
|
||||
if (seconds < 60) {
|
||||
ImGui::Text("%ds", seconds);
|
||||
} else {
|
||||
ImGui::Text("%dm", seconds / 60);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
shown++;
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Loot Window (Phase 5)
|
||||
// ============================================================
|
||||
|
||||
void GameScreen::renderLootWindow(game::GameHandler& gameHandler) {
|
||||
if (!gameHandler.isLootWindowOpen()) return;
|
||||
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 200), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Always);
|
||||
|
||||
bool open = true;
|
||||
if (ImGui::Begin("Loot", &open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
const auto& loot = gameHandler.getCurrentLoot();
|
||||
|
||||
// Gold
|
||||
if (loot.gold > 0) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%ug %us %uc",
|
||||
loot.getGold(), loot.getSilver(), loot.getCopper());
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
// Items
|
||||
for (const auto& item : loot.items) {
|
||||
ImGui::PushID(item.slotIndex);
|
||||
char label[64];
|
||||
snprintf(label, sizeof(label), "Item %u (x%u)", item.itemId, item.count);
|
||||
if (ImGui::Selectable(label)) {
|
||||
gameHandler.lootItem(item.slotIndex);
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
if (loot.items.empty() && loot.gold == 0) {
|
||||
ImGui::TextDisabled("Empty");
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("Close", ImVec2(-1, 0))) {
|
||||
gameHandler.closeLoot();
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
if (!open) {
|
||||
gameHandler.closeLoot();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Gossip Window (Phase 5)
|
||||
// ============================================================
|
||||
|
||||
void GameScreen::renderGossipWindow(game::GameHandler& gameHandler) {
|
||||
if (!gameHandler.isGossipWindowOpen()) return;
|
||||
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 150), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_Always);
|
||||
|
||||
bool open = true;
|
||||
if (ImGui::Begin("NPC Dialog", &open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
const auto& gossip = gameHandler.getCurrentGossip();
|
||||
|
||||
// NPC name (from creature cache)
|
||||
auto npcEntity = gameHandler.getEntityManager().getEntity(gossip.npcGuid);
|
||||
if (npcEntity && npcEntity->getType() == game::ObjectType::UNIT) {
|
||||
auto unit = std::static_pointer_cast<game::Unit>(npcEntity);
|
||||
if (!unit->getName().empty()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%s", unit->getName().c_str());
|
||||
ImGui::Separator();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Gossip options
|
||||
static const char* gossipIcons[] = {"[Chat]", "[Vendor]", "[Taxi]", "[Trainer]", "[Spiritguide]",
|
||||
"[Tabardvendor]", "[Battlemaster]", "[Banker]", "[Petitioner]",
|
||||
"[Tabarddesigner]", "[Auctioneer]"};
|
||||
|
||||
for (const auto& opt : gossip.options) {
|
||||
ImGui::PushID(static_cast<int>(opt.id));
|
||||
const char* icon = (opt.icon < 11) ? gossipIcons[opt.icon] : "[Option]";
|
||||
char label[256];
|
||||
snprintf(label, sizeof(label), "%s %s", icon, opt.text.c_str());
|
||||
if (ImGui::Selectable(label)) {
|
||||
gameHandler.selectGossipOption(opt.id);
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
// Quest items
|
||||
if (!gossip.quests.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Quests:");
|
||||
for (const auto& quest : gossip.quests) {
|
||||
ImGui::BulletText("[%d] %s", quest.questLevel, quest.title.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("Close", ImVec2(-1, 0))) {
|
||||
gameHandler.closeGossip();
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
if (!open) {
|
||||
gameHandler.closeGossip();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Vendor Window (Phase 5)
|
||||
// ============================================================
|
||||
|
||||
void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) {
|
||||
if (!gameHandler.isVendorWindowOpen()) return;
|
||||
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 100), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(450, 400), ImGuiCond_FirstUseEver);
|
||||
|
||||
bool open = true;
|
||||
if (ImGui::Begin("Vendor", &open)) {
|
||||
const auto& vendor = gameHandler.getVendorItems();
|
||||
|
||||
if (vendor.items.empty()) {
|
||||
ImGui::TextDisabled("This vendor has nothing for sale.");
|
||||
} else {
|
||||
if (ImGui::BeginTable("VendorTable", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) {
|
||||
ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("Price", ImGuiTableColumnFlags_WidthFixed, 120.0f);
|
||||
ImGui::TableSetupColumn("Stock", ImGuiTableColumnFlags_WidthFixed, 60.0f);
|
||||
ImGui::TableSetupColumn("Buy", ImGuiTableColumnFlags_WidthFixed, 50.0f);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
for (const auto& item : vendor.items) {
|
||||
ImGui::TableNextRow();
|
||||
ImGui::PushID(static_cast<int>(item.slot));
|
||||
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::Text("Item %u", item.itemId);
|
||||
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
uint32_t g = item.buyPrice / 10000;
|
||||
uint32_t s = (item.buyPrice / 100) % 100;
|
||||
uint32_t c = item.buyPrice % 100;
|
||||
ImGui::Text("%ug %us %uc", g, s, c);
|
||||
|
||||
ImGui::TableSetColumnIndex(2);
|
||||
if (item.maxCount < 0) {
|
||||
ImGui::Text("Inf");
|
||||
} else {
|
||||
ImGui::Text("%d", item.maxCount);
|
||||
}
|
||||
|
||||
ImGui::TableSetColumnIndex(3);
|
||||
if (ImGui::SmallButton("Buy")) {
|
||||
gameHandler.buyItem(vendor.vendorGuid, item.itemId, item.slot, 1);
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
if (!open) {
|
||||
// Close vendor - just hide UI, no server packet needed
|
||||
// The vendor window state will be reset on next interaction
|
||||
}
|
||||
}
|
||||
|
||||
}} // namespace wowee::ui
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue