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:
Kelsi 2026-02-04 10:30:52 -08:00
parent 6bf3fa4ed4
commit c49bb58e47
14 changed files with 3039 additions and 84 deletions

View file

@ -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;