mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Fix online mode combat and implement server inventory sync
Right-click now attacks hostile NPCs (npcFlags==0) and interacts with friendly ones in online mode. Parse UNIT_FIELD_FLAGS (59) and UNIT_NPC_FLAGS (82) from update packets. Stop auto-attack when target dies or despawns. Add CMSG_ITEM_QUERY_SINGLE/SMSG_ITEM_QUERY_SINGLE_RESPONSE to populate inventory from server item objects and player slot fields.
This commit is contained in:
parent
4d80b92c39
commit
ab1f39c73b
7 changed files with 397 additions and 15 deletions
|
|
@ -151,6 +151,17 @@ public:
|
|||
uint32_t getDisplayId() const { return displayId; }
|
||||
void setDisplayId(uint32_t id) { displayId = id; }
|
||||
|
||||
// Unit flags (UNIT_FIELD_FLAGS, index 59)
|
||||
uint32_t getUnitFlags() const { return unitFlags; }
|
||||
void setUnitFlags(uint32_t f) { unitFlags = f; }
|
||||
|
||||
// NPC flags (UNIT_NPC_FLAGS, index 82)
|
||||
uint32_t getNpcFlags() const { return npcFlags; }
|
||||
void setNpcFlags(uint32_t f) { npcFlags = f; }
|
||||
|
||||
// Returns true if NPC has interaction flags (gossip/vendor/quest/trainer)
|
||||
bool isInteractable() const { return npcFlags != 0; }
|
||||
|
||||
protected:
|
||||
std::string name;
|
||||
uint32_t health = 0;
|
||||
|
|
@ -161,6 +172,8 @@ protected:
|
|||
uint32_t level = 1;
|
||||
uint32_t entry = 0;
|
||||
uint32_t displayId = 0;
|
||||
uint32_t unitFlags = 0;
|
||||
uint32_t npcFlags = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -410,6 +410,9 @@ private:
|
|||
// ---- Phase 1 handlers ----
|
||||
void handleNameQueryResponse(network::Packet& packet);
|
||||
void handleCreatureQueryResponse(network::Packet& packet);
|
||||
void handleItemQueryResponse(network::Packet& packet);
|
||||
void queryItemInfo(uint32_t entry, uint64_t guid);
|
||||
void rebuildOnlineInventory();
|
||||
|
||||
// ---- Phase 2 handlers ----
|
||||
void handleAttackStart(network::Packet& packet);
|
||||
|
|
@ -535,6 +538,17 @@ private:
|
|||
std::unordered_map<uint32_t, CreatureQueryResponseData> creatureInfoCache;
|
||||
std::unordered_set<uint32_t> pendingCreatureQueries;
|
||||
|
||||
// ---- Online item tracking ----
|
||||
struct OnlineItemInfo {
|
||||
uint32_t entry = 0;
|
||||
uint32_t stackCount = 1;
|
||||
};
|
||||
std::unordered_map<uint64_t, OnlineItemInfo> onlineItems_;
|
||||
std::unordered_map<uint32_t, ItemQueryResponseData> itemInfoCache_;
|
||||
std::unordered_set<uint32_t> pendingItemQueries_;
|
||||
std::array<uint64_t, 23> equipSlotGuids_{};
|
||||
std::array<uint64_t, 16> backpackSlotGuids_{};
|
||||
|
||||
// ---- Phase 2: Combat ----
|
||||
bool autoAttacking = false;
|
||||
uint64_t autoAttackTarget = 0;
|
||||
|
|
|
|||
|
|
@ -139,6 +139,8 @@ enum class Opcode : uint16_t {
|
|||
SMSG_BUY_FAILED = 0x1A5,
|
||||
|
||||
// ---- Phase 5: Item/Equip ----
|
||||
CMSG_ITEM_QUERY_SINGLE = 0x056,
|
||||
SMSG_ITEM_QUERY_SINGLE_RESPONSE = 0x058,
|
||||
CMSG_AUTOEQUIP_ITEM = 0x10A,
|
||||
SMSG_INVENTORY_CHANGE_FAILURE = 0x112,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -715,6 +715,41 @@ public:
|
|||
static bool parse(network::Packet& packet, CreatureQueryResponseData& data);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Item Query
|
||||
// ============================================================
|
||||
|
||||
/** CMSG_ITEM_QUERY_SINGLE packet builder */
|
||||
class ItemQueryPacket {
|
||||
public:
|
||||
static network::Packet build(uint32_t entry, uint64_t guid);
|
||||
};
|
||||
|
||||
/** SMSG_ITEM_QUERY_SINGLE_RESPONSE data */
|
||||
struct ItemQueryResponseData {
|
||||
uint32_t entry = 0;
|
||||
std::string name;
|
||||
uint32_t displayInfoId = 0;
|
||||
uint32_t quality = 0;
|
||||
uint32_t inventoryType = 0;
|
||||
int32_t maxStack = 1;
|
||||
uint32_t containerSlots = 0;
|
||||
int32_t armor = 0;
|
||||
int32_t stamina = 0;
|
||||
int32_t strength = 0;
|
||||
int32_t agility = 0;
|
||||
int32_t intellect = 0;
|
||||
int32_t spirit = 0;
|
||||
std::string subclassName;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
/** SMSG_ITEM_QUERY_SINGLE_RESPONSE parser */
|
||||
class ItemQueryResponseParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, ItemQueryResponseData& data);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Phase 2: Combat Core
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -964,6 +964,10 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
handleCreatureQueryResponse(packet);
|
||||
break;
|
||||
|
||||
case Opcode::SMSG_ITEM_QUERY_SINGLE_RESPONSE:
|
||||
handleItemQueryResponse(packet);
|
||||
break;
|
||||
|
||||
// ---- XP ----
|
||||
case Opcode::SMSG_LOG_XPGAIN:
|
||||
handleXpGain(packet);
|
||||
|
|
@ -2382,8 +2386,10 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
case 25: unit->setPower(val); break;
|
||||
case 32: unit->setMaxHealth(val); break;
|
||||
case 33: unit->setMaxPower(val); break;
|
||||
case 59: unit->setUnitFlags(val); break; // UNIT_FIELD_FLAGS
|
||||
case 54: unit->setLevel(val); break;
|
||||
case 67: unit->setDisplayId(val); break; // UNIT_FIELD_DISPLAYID
|
||||
case 82: unit->setNpcFlags(val); break; // UNIT_NPC_FLAGS
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
|
@ -2395,16 +2401,50 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
}
|
||||
// Extract XP fields for player entity
|
||||
// Track online item objects
|
||||
if (block.objectType == ObjectType::ITEM) {
|
||||
auto entryIt = block.fields.find(3); // OBJECT_FIELD_ENTRY
|
||||
auto stackIt = block.fields.find(14); // ITEM_FIELD_STACK_COUNT
|
||||
if (entryIt != block.fields.end() && entryIt->second != 0) {
|
||||
OnlineItemInfo info;
|
||||
info.entry = entryIt->second;
|
||||
info.stackCount = (stackIt != block.fields.end()) ? stackIt->second : 1;
|
||||
onlineItems_[block.guid] = info;
|
||||
queryItemInfo(info.entry, block.guid);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract XP / inventory slot fields for player entity
|
||||
if (block.guid == playerGuid && block.objectType == ObjectType::PLAYER) {
|
||||
bool slotsChanged = false;
|
||||
for (const auto& [key, val] : block.fields) {
|
||||
switch (key) {
|
||||
case 634: playerXp_ = val; break; // PLAYER_XP
|
||||
case 635: playerNextLevelXp_ = val; break; // PLAYER_NEXT_LEVEL_XP
|
||||
case 54: serverPlayerLevel_ = val; break; // UNIT_FIELD_LEVEL
|
||||
default: break;
|
||||
if (key == 634) { playerXp_ = val; } // PLAYER_XP
|
||||
else if (key == 635) { playerNextLevelXp_ = val; } // PLAYER_NEXT_LEVEL_XP
|
||||
else if (key == 54) { serverPlayerLevel_ = val; } // UNIT_FIELD_LEVEL
|
||||
else if (key == 632) { playerMoneyCopper_ = val; } // PLAYER_FIELD_COINAGE
|
||||
else if (key >= 322 && key <= 367) {
|
||||
// PLAYER_FIELD_INV_SLOT_HEAD: equipment slots (23 slots × 2 fields)
|
||||
int slotIndex = (key - 322) / 2;
|
||||
bool isLow = ((key - 322) % 2 == 0);
|
||||
if (slotIndex < 23) {
|
||||
uint64_t& guid = equipSlotGuids_[slotIndex];
|
||||
if (isLow) guid = (guid & 0xFFFFFFFF00000000ULL) | val;
|
||||
else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32);
|
||||
slotsChanged = true;
|
||||
}
|
||||
} else if (key >= 368 && key <= 399) {
|
||||
// PLAYER_FIELD_PACK_SLOT_1: backpack slots (16 slots × 2 fields)
|
||||
int slotIndex = (key - 368) / 2;
|
||||
bool isLow = ((key - 368) % 2 == 0);
|
||||
if (slotIndex < 16) {
|
||||
uint64_t& guid = backpackSlotGuids_[slotIndex];
|
||||
if (isLow) guid = (guid & 0xFFFFFFFF00000000ULL) | val;
|
||||
else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32);
|
||||
slotsChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (slotsChanged) rebuildOnlineInventory();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -2422,25 +2462,62 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||
for (const auto& [key, val] : block.fields) {
|
||||
switch (key) {
|
||||
case 24: unit->setHealth(val); break;
|
||||
case 24:
|
||||
unit->setHealth(val);
|
||||
if (val == 0 && block.guid == autoAttackTarget) {
|
||||
stopAutoAttack();
|
||||
}
|
||||
break;
|
||||
case 25: unit->setPower(val); break;
|
||||
case 32: unit->setMaxHealth(val); break;
|
||||
case 33: unit->setMaxPower(val); break;
|
||||
case 59: unit->setUnitFlags(val); break; // UNIT_FIELD_FLAGS
|
||||
case 54: unit->setLevel(val); break;
|
||||
case 82: unit->setNpcFlags(val); break; // UNIT_NPC_FLAGS
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update XP fields for player entity
|
||||
// Update XP / inventory slot fields for player entity
|
||||
if (block.guid == playerGuid) {
|
||||
bool slotsChanged = false;
|
||||
for (const auto& [key, val] : block.fields) {
|
||||
switch (key) {
|
||||
case 634: playerXp_ = val; break; // PLAYER_XP
|
||||
case 635: playerNextLevelXp_ = val; break; // PLAYER_NEXT_LEVEL_XP
|
||||
case 54: serverPlayerLevel_ = val; break; // UNIT_FIELD_LEVEL
|
||||
default: break;
|
||||
if (key == 634) { playerXp_ = val; }
|
||||
else if (key == 635) { playerNextLevelXp_ = val; }
|
||||
else if (key == 54) { serverPlayerLevel_ = val; }
|
||||
else if (key == 632) { playerMoneyCopper_ = val; }
|
||||
else if (key >= 322 && key <= 367) {
|
||||
int slotIndex = (key - 322) / 2;
|
||||
bool isLow = ((key - 322) % 2 == 0);
|
||||
if (slotIndex < 23) {
|
||||
uint64_t& guid = equipSlotGuids_[slotIndex];
|
||||
if (isLow) guid = (guid & 0xFFFFFFFF00000000ULL) | val;
|
||||
else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32);
|
||||
slotsChanged = true;
|
||||
}
|
||||
} else if (key >= 368 && key <= 399) {
|
||||
int slotIndex = (key - 368) / 2;
|
||||
bool isLow = ((key - 368) % 2 == 0);
|
||||
if (slotIndex < 16) {
|
||||
uint64_t& guid = backpackSlotGuids_[slotIndex];
|
||||
if (isLow) guid = (guid & 0xFFFFFFFF00000000ULL) | val;
|
||||
else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32);
|
||||
slotsChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (slotsChanged) rebuildOnlineInventory();
|
||||
}
|
||||
|
||||
// Update item stack count for online items
|
||||
if (entity->getType() == ObjectType::ITEM) {
|
||||
for (const auto& [key, val] : block.fields) {
|
||||
if (key == 14) { // ITEM_FIELD_STACK_COUNT
|
||||
auto it = onlineItems_.find(block.guid);
|
||||
if (it != onlineItems_.end()) it->second.stackCount = val;
|
||||
}
|
||||
}
|
||||
rebuildOnlineInventory();
|
||||
}
|
||||
|
||||
LOG_DEBUG("Updated entity fields: 0x", std::hex, block.guid, std::dec);
|
||||
|
|
@ -2528,6 +2605,19 @@ void GameHandler::handleDestroyObject(network::Packet& packet) {
|
|||
LOG_WARNING("Destroy object for unknown entity: 0x", std::hex, data.guid, std::dec);
|
||||
}
|
||||
|
||||
// Clean up auto-attack and target if destroyed entity was our target
|
||||
if (data.guid == autoAttackTarget) {
|
||||
stopAutoAttack();
|
||||
}
|
||||
if (data.guid == targetGuid) {
|
||||
targetGuid = 0;
|
||||
}
|
||||
|
||||
// Remove online item tracking
|
||||
if (onlineItems_.erase(data.guid)) {
|
||||
rebuildOnlineInventory();
|
||||
}
|
||||
|
||||
tabCycleStale = true;
|
||||
LOG_INFO("Entity count: ", entityManager.getEntityCount());
|
||||
}
|
||||
|
|
@ -2748,6 +2838,111 @@ void GameHandler::handleCreatureQueryResponse(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Item Query
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::queryItemInfo(uint32_t entry, uint64_t guid) {
|
||||
if (itemInfoCache_.count(entry) || pendingItemQueries_.count(entry)) return;
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
|
||||
pendingItemQueries_.insert(entry);
|
||||
auto packet = ItemQueryPacket::build(entry, guid);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::handleItemQueryResponse(network::Packet& packet) {
|
||||
ItemQueryResponseData data;
|
||||
if (!ItemQueryResponseParser::parse(packet, data)) return;
|
||||
|
||||
pendingItemQueries_.erase(data.entry);
|
||||
|
||||
if (data.valid) {
|
||||
itemInfoCache_[data.entry] = data;
|
||||
rebuildOnlineInventory();
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::rebuildOnlineInventory() {
|
||||
if (singlePlayerMode_) return;
|
||||
|
||||
inventory = Inventory();
|
||||
|
||||
// Equipment slots
|
||||
for (int i = 0; i < 23; i++) {
|
||||
uint64_t guid = equipSlotGuids_[i];
|
||||
if (guid == 0) continue;
|
||||
|
||||
auto itemIt = onlineItems_.find(guid);
|
||||
if (itemIt == onlineItems_.end()) continue;
|
||||
|
||||
ItemDef def;
|
||||
def.itemId = itemIt->second.entry;
|
||||
def.stackCount = itemIt->second.stackCount;
|
||||
def.maxStack = 1;
|
||||
|
||||
auto infoIt = itemInfoCache_.find(itemIt->second.entry);
|
||||
if (infoIt != itemInfoCache_.end()) {
|
||||
def.name = infoIt->second.name;
|
||||
def.quality = static_cast<ItemQuality>(infoIt->second.quality);
|
||||
def.inventoryType = infoIt->second.inventoryType;
|
||||
def.maxStack = std::max(1, infoIt->second.maxStack);
|
||||
def.displayInfoId = infoIt->second.displayInfoId;
|
||||
def.subclassName = infoIt->second.subclassName;
|
||||
def.armor = infoIt->second.armor;
|
||||
def.stamina = infoIt->second.stamina;
|
||||
def.strength = infoIt->second.strength;
|
||||
def.agility = infoIt->second.agility;
|
||||
def.intellect = infoIt->second.intellect;
|
||||
def.spirit = infoIt->second.spirit;
|
||||
} else {
|
||||
def.name = "Item " + std::to_string(def.itemId);
|
||||
}
|
||||
|
||||
inventory.setEquipSlot(static_cast<EquipSlot>(i), def);
|
||||
}
|
||||
|
||||
// Backpack slots
|
||||
for (int i = 0; i < 16; i++) {
|
||||
uint64_t guid = backpackSlotGuids_[i];
|
||||
if (guid == 0) continue;
|
||||
|
||||
auto itemIt = onlineItems_.find(guid);
|
||||
if (itemIt == onlineItems_.end()) continue;
|
||||
|
||||
ItemDef def;
|
||||
def.itemId = itemIt->second.entry;
|
||||
def.stackCount = itemIt->second.stackCount;
|
||||
def.maxStack = 1;
|
||||
|
||||
auto infoIt = itemInfoCache_.find(itemIt->second.entry);
|
||||
if (infoIt != itemInfoCache_.end()) {
|
||||
def.name = infoIt->second.name;
|
||||
def.quality = static_cast<ItemQuality>(infoIt->second.quality);
|
||||
def.inventoryType = infoIt->second.inventoryType;
|
||||
def.maxStack = std::max(1, infoIt->second.maxStack);
|
||||
def.displayInfoId = infoIt->second.displayInfoId;
|
||||
def.subclassName = infoIt->second.subclassName;
|
||||
def.armor = infoIt->second.armor;
|
||||
def.stamina = infoIt->second.stamina;
|
||||
def.strength = infoIt->second.strength;
|
||||
def.agility = infoIt->second.agility;
|
||||
def.intellect = infoIt->second.intellect;
|
||||
def.spirit = infoIt->second.spirit;
|
||||
} else {
|
||||
def.name = "Item " + std::to_string(def.itemId);
|
||||
}
|
||||
|
||||
inventory.setBackpackSlot(i, def);
|
||||
}
|
||||
|
||||
LOG_DEBUG("Rebuilt online inventory: equip=", [&](){
|
||||
int c = 0; for (auto g : equipSlotGuids_) if (g) c++; return c;
|
||||
}(), " backpack=", [&](){
|
||||
int c = 0; for (auto g : backpackSlotGuids_) if (g) c++; return c;
|
||||
}());
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 2: Combat
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -1207,6 +1207,121 @@ bool CreatureQueryResponseParser::parse(network::Packet& packet, CreatureQueryRe
|
|||
return true;
|
||||
}
|
||||
|
||||
// ---- Item Query ----
|
||||
|
||||
network::Packet ItemQueryPacket::build(uint32_t entry, uint64_t guid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_ITEM_QUERY_SINGLE));
|
||||
packet.writeUInt32(entry);
|
||||
packet.writeUInt64(guid);
|
||||
LOG_DEBUG("Built CMSG_ITEM_QUERY_SINGLE: entry=", entry, " guid=0x", std::hex, guid, std::dec);
|
||||
return packet;
|
||||
}
|
||||
|
||||
static const char* getItemSubclassName(uint32_t itemClass, uint32_t subClass) {
|
||||
if (itemClass == 2) { // Weapon
|
||||
switch (subClass) {
|
||||
case 0: return "Axe"; case 1: return "Axe";
|
||||
case 2: return "Bow"; case 3: return "Gun";
|
||||
case 4: return "Mace"; case 5: return "Mace";
|
||||
case 6: return "Polearm"; case 7: return "Sword";
|
||||
case 8: return "Sword"; case 9: return "Obsolete";
|
||||
case 10: return "Staff"; case 13: return "Fist Weapon";
|
||||
case 15: return "Dagger"; case 16: return "Thrown";
|
||||
case 18: return "Crossbow"; case 19: return "Wand";
|
||||
case 20: return "Fishing Pole";
|
||||
default: return "Weapon";
|
||||
}
|
||||
}
|
||||
if (itemClass == 4) { // Armor
|
||||
switch (subClass) {
|
||||
case 0: return "Miscellaneous"; case 1: return "Cloth";
|
||||
case 2: return "Leather"; case 3: return "Mail";
|
||||
case 4: return "Plate"; case 6: return "Shield";
|
||||
default: return "Armor";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseData& data) {
|
||||
data.entry = packet.readUInt32();
|
||||
|
||||
// High bit set means item not found
|
||||
if (data.entry & 0x80000000) {
|
||||
data.entry &= ~0x80000000;
|
||||
LOG_DEBUG("Item query: entry ", data.entry, " not found");
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t itemClass = packet.readUInt32();
|
||||
uint32_t subClass = packet.readUInt32();
|
||||
packet.readUInt32(); // SoundOverrideSubclass
|
||||
|
||||
data.subclassName = getItemSubclassName(itemClass, subClass);
|
||||
|
||||
// 4 name strings
|
||||
data.name = packet.readString();
|
||||
packet.readString(); // name2
|
||||
packet.readString(); // name3
|
||||
packet.readString(); // name4
|
||||
|
||||
data.displayInfoId = packet.readUInt32();
|
||||
data.quality = packet.readUInt32();
|
||||
|
||||
packet.readUInt32(); // Flags
|
||||
packet.readUInt32(); // Flags2
|
||||
packet.readUInt32(); // BuyPrice
|
||||
packet.readUInt32(); // SellPrice
|
||||
|
||||
data.inventoryType = packet.readUInt32();
|
||||
|
||||
packet.readUInt32(); // AllowableClass
|
||||
packet.readUInt32(); // AllowableRace
|
||||
packet.readUInt32(); // ItemLevel
|
||||
packet.readUInt32(); // RequiredLevel
|
||||
packet.readUInt32(); // RequiredSkill
|
||||
packet.readUInt32(); // RequiredSkillRank
|
||||
packet.readUInt32(); // RequiredSpell
|
||||
packet.readUInt32(); // RequiredHonorRank
|
||||
packet.readUInt32(); // RequiredCityRank
|
||||
packet.readUInt32(); // RequiredReputationFaction
|
||||
packet.readUInt32(); // RequiredReputationRank
|
||||
packet.readUInt32(); // MaxCount
|
||||
data.maxStack = static_cast<int32_t>(packet.readUInt32()); // Stackable
|
||||
data.containerSlots = packet.readUInt32();
|
||||
|
||||
uint32_t statsCount = packet.readUInt32();
|
||||
for (uint32_t i = 0; i < statsCount && i < 10; i++) {
|
||||
uint32_t statType = packet.readUInt32();
|
||||
int32_t statValue = static_cast<int32_t>(packet.readUInt32());
|
||||
switch (statType) {
|
||||
case 3: data.agility = statValue; break;
|
||||
case 4: data.strength = statValue; break;
|
||||
case 5: data.intellect = statValue; break;
|
||||
case 6: data.spirit = statValue; break;
|
||||
case 7: data.stamina = statValue; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
packet.readUInt32(); // ScalingStatDistribution
|
||||
packet.readUInt32(); // ScalingStatValue
|
||||
|
||||
// 5 damage types
|
||||
for (int i = 0; i < 5; i++) {
|
||||
packet.readFloat(); // DamageMin
|
||||
packet.readFloat(); // DamageMax
|
||||
packet.readUInt32(); // DamageType
|
||||
}
|
||||
|
||||
data.armor = static_cast<int32_t>(packet.readUInt32());
|
||||
|
||||
data.valid = !data.name.empty();
|
||||
LOG_INFO("Item query response: ", data.name, " (quality=", data.quality,
|
||||
" invType=", data.inventoryType, " stack=", data.maxStack, ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 2: Combat Core
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -462,8 +462,16 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
gameHandler.startAutoAttack(target->getGuid());
|
||||
}
|
||||
} else {
|
||||
// Try NPC interaction first (gossip), fall back to attack
|
||||
gameHandler.interactWithNpc(target->getGuid());
|
||||
// Online mode: interact with friendly NPCs, attack hostiles
|
||||
if (unit->isInteractable()) {
|
||||
gameHandler.interactWithNpc(target->getGuid());
|
||||
} else {
|
||||
if (gameHandler.isAutoAttacking()) {
|
||||
gameHandler.stopAutoAttack();
|
||||
} else {
|
||||
gameHandler.startAutoAttack(target->getGuid());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (target->getType() == game::ObjectType::PLAYER) {
|
||||
// Right-click another player could start attack in PvP context
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue