mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Fix trainer system and add critical spell/quest opcodes
Trainer System Fixes: - Fix CMSG_TRAINER_BUY_SPELL packet: remove incorrect trainerType field (12 bytes not 16) - Correct spell state interpretation: 0=available, 1=unavailable, 2=known - Add dynamic prerequisite re-evaluation in real-time as spells are learned - Immediately update knownSpells on SMSG_TRAINER_BUY_SUCCEEDED - Add "Show unavailable spells" checkbox filter to trainer window - Override server state when prerequisites become met client-side New Spell Opcodes: - SMSG_SUPERCEDED_SPELL (0x12C): handle spell rank upgrades - SMSG_SEND_UNLEARN_SPELLS (0x41F): handle bulk unlearning (respec/dual-spec) - CMSG_TRAINER_LIST (0x1B0): trainer request opcode Quest System: - SMSG_QUESTUPDATE_COMPLETE (0x195): mark quests complete when objectives done - Show "Quest Complete" message and enable turn-in UI Detailed logging: - SMSG_INITIAL_SPELLS now logs packet size and first 10 spell IDs - Money values logged during trainer purchases - Trainer spell states and prerequisites logged for debugging This enables proper spell progression chains, spec changes, and quest completion notifications matching retail WoW 3.3.5a behavior.
This commit is contained in:
parent
8af895c025
commit
a764eea2ec
6 changed files with 632 additions and 36 deletions
|
|
@ -495,6 +495,8 @@ public:
|
|||
std::string title;
|
||||
std::string objectives;
|
||||
bool complete = false;
|
||||
// Objective kill counts: objectiveIndex -> (current, required)
|
||||
std::unordered_map<uint32_t, std::pair<uint32_t, uint32_t>> killCounts;
|
||||
};
|
||||
const std::vector<QuestLogEntry>& getQuestLog() const { return questLog_; }
|
||||
void abandonQuest(uint32_t questId);
|
||||
|
|
@ -697,7 +699,9 @@ private:
|
|||
void handleCooldownEvent(network::Packet& packet);
|
||||
void handleAuraUpdate(network::Packet& packet, bool isAll);
|
||||
void handleLearnedSpell(network::Packet& packet);
|
||||
void handleSupercededSpell(network::Packet& packet);
|
||||
void handleRemovedSpell(network::Packet& packet);
|
||||
void handleUnlearnSpells(network::Packet& packet);
|
||||
|
||||
// ---- Phase 4 handlers ----
|
||||
void handleGroupInvite(network::Packet& packet);
|
||||
|
|
|
|||
|
|
@ -159,7 +159,9 @@ enum class Opcode : uint16_t {
|
|||
SMSG_UPDATE_AURA_DURATION = 0x137,
|
||||
SMSG_INITIAL_SPELLS = 0x12A,
|
||||
SMSG_LEARNED_SPELL = 0x12B,
|
||||
SMSG_SUPERCEDED_SPELL = 0x12C,
|
||||
SMSG_REMOVED_SPELL = 0x203,
|
||||
SMSG_SEND_UNLEARN_SPELLS = 0x41F,
|
||||
SMSG_SPELL_DELAYED = 0x1E2,
|
||||
SMSG_AURA_UPDATE = 0x3FA,
|
||||
SMSG_AURA_UPDATE_ALL = 0x495,
|
||||
|
|
@ -223,8 +225,11 @@ enum class Opcode : uint16_t {
|
|||
SMSG_QUESTGIVER_QUEST_INVALID = 0x18F,
|
||||
SMSG_QUESTGIVER_QUEST_COMPLETE = 0x191,
|
||||
CMSG_QUESTLOG_REMOVE_QUEST = 0x194,
|
||||
SMSG_QUESTUPDATE_ADD_KILL = 0x196, // Quest kill count update
|
||||
SMSG_QUESTUPDATE_COMPLETE = 0x195, // Quest objectives completed
|
||||
CMSG_QUEST_QUERY = 0x05C, // Client requests quest data
|
||||
SMSG_QUEST_QUERY_RESPONSE = 0x05D, // Server sends quest data
|
||||
SMSG_QUESTLOG_FULL = 0x1A3, // Full quest log on login
|
||||
|
||||
// ---- Phase 5: Vendor ----
|
||||
CMSG_LIST_INVENTORY = 0x19E,
|
||||
|
|
@ -235,8 +240,10 @@ enum class Opcode : uint16_t {
|
|||
SMSG_BUY_FAILED = 0x1A5,
|
||||
|
||||
// ---- Trainer ----
|
||||
CMSG_TRAINER_LIST = 0x01B0,
|
||||
SMSG_TRAINER_LIST = 0x01B1,
|
||||
CMSG_TRAINER_BUY_SPELL = 0x01B2,
|
||||
SMSG_TRAINER_BUY_FAILED = 0x01B4,
|
||||
|
||||
// ---- Phase 5: Item/Equip ----
|
||||
CMSG_ITEM_QUERY_SINGLE = 0x056,
|
||||
|
|
|
|||
|
|
@ -1750,7 +1750,7 @@ public:
|
|||
|
||||
class TrainerBuySpellPacket {
|
||||
public:
|
||||
static network::Packet build(uint64_t trainerGuid, uint32_t trainerId, uint32_t spellId);
|
||||
static network::Packet build(uint64_t trainerGuid, uint32_t spellId);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -579,9 +579,15 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
case Opcode::SMSG_LEARNED_SPELL:
|
||||
handleLearnedSpell(packet);
|
||||
break;
|
||||
case Opcode::SMSG_SUPERCEDED_SPELL:
|
||||
handleSupercededSpell(packet);
|
||||
break;
|
||||
case Opcode::SMSG_REMOVED_SPELL:
|
||||
handleRemovedSpell(packet);
|
||||
break;
|
||||
case Opcode::SMSG_SEND_UNLEARN_SPELLS:
|
||||
handleUnlearnSpells(packet);
|
||||
break;
|
||||
|
||||
// ---- Phase 4: Group ----
|
||||
case Opcode::SMSG_GROUP_INVITE:
|
||||
|
|
@ -682,6 +688,15 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
uint64_t guid = packet.readUInt64();
|
||||
uint32_t spellId = packet.readUInt32();
|
||||
(void)guid;
|
||||
|
||||
// Add to known spells immediately for prerequisite re-evaluation
|
||||
// (SMSG_LEARNED_SPELL may come separately, but we need immediate update)
|
||||
bool alreadyKnown = std::find(knownSpells.begin(), knownSpells.end(), spellId) != knownSpells.end();
|
||||
if (!alreadyKnown) {
|
||||
knownSpells.push_back(spellId);
|
||||
LOG_INFO("Added spell ", spellId, " to known spells (trainer purchase)");
|
||||
}
|
||||
|
||||
const std::string& name = getSpellName(spellId);
|
||||
if (!name.empty())
|
||||
addSystemChatMessage("You have learned " + name + ".");
|
||||
|
|
@ -689,6 +704,32 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
addSystemChatMessage("Spell learned.");
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_TRAINER_BUY_FAILED: {
|
||||
// Server rejected the spell purchase
|
||||
// Packet format: uint64 trainerGuid, uint32 spellId, uint32 errorCode
|
||||
uint64_t trainerGuid = packet.readUInt64();
|
||||
uint32_t spellId = packet.readUInt32();
|
||||
uint32_t errorCode = 0;
|
||||
if (packet.getSize() - packet.getReadPos() >= 4) {
|
||||
errorCode = packet.readUInt32();
|
||||
}
|
||||
LOG_WARNING("Trainer buy spell failed: guid=", trainerGuid,
|
||||
" spellId=", spellId, " error=", errorCode);
|
||||
|
||||
const std::string& spellName = getSpellName(spellId);
|
||||
std::string msg = "Cannot learn ";
|
||||
if (!spellName.empty()) msg += spellName;
|
||||
else msg += "spell #" + std::to_string(spellId);
|
||||
|
||||
// Common error reasons
|
||||
if (errorCode == 0) msg += " (not enough money)";
|
||||
else if (errorCode == 1) msg += " (not enough skill)";
|
||||
else if (errorCode == 2) msg += " (already known)";
|
||||
else if (errorCode != 0) msg += " (error " + std::to_string(errorCode) + ")";
|
||||
|
||||
addSystemChatMessage(msg);
|
||||
break;
|
||||
}
|
||||
|
||||
// Silently ignore common packets we don't handle yet
|
||||
case Opcode::SMSG_FEATURE_SYSTEM_STATUS:
|
||||
|
|
@ -851,8 +892,10 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
case 19: reasonStr = "Can't take any more quests"; break;
|
||||
}
|
||||
LOG_WARNING("Quest invalid: reason=", failReason, " (", reasonStr, ")");
|
||||
// Show error to user
|
||||
addSystemChatMessage(std::string("Quest unavailable: ") + reasonStr);
|
||||
// Only show error to user for real errors (not informational messages)
|
||||
if (failReason != 13 && failReason != 18) { // Don't spam "already on/completed"
|
||||
addSystemChatMessage(std::string("Quest unavailable: ") + reasonStr);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -883,6 +926,52 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_QUESTUPDATE_ADD_KILL: {
|
||||
// Quest kill count update
|
||||
if (packet.getSize() - packet.getReadPos() >= 16) {
|
||||
uint32_t questId = packet.readUInt32();
|
||||
uint32_t entry = packet.readUInt32(); // Creature entry
|
||||
uint32_t count = packet.readUInt32(); // Current kills
|
||||
uint32_t reqCount = packet.readUInt32(); // Required kills
|
||||
|
||||
LOG_INFO("Quest kill update: questId=", questId, " entry=", entry,
|
||||
" count=", count, "/", reqCount);
|
||||
|
||||
// Update quest log with kill count
|
||||
for (auto& quest : questLog_) {
|
||||
if (quest.questId == questId) {
|
||||
// Store kill progress (using entry as objective index)
|
||||
quest.killCounts[entry] = {count, reqCount};
|
||||
|
||||
// Show progress message
|
||||
std::string progressMsg = quest.title + ": " +
|
||||
std::to_string(count) + "/" +
|
||||
std::to_string(reqCount);
|
||||
addSystemChatMessage(progressMsg);
|
||||
|
||||
LOG_INFO("Updated kill count for quest ", questId, ": ",
|
||||
count, "/", reqCount);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_QUESTUPDATE_COMPLETE: {
|
||||
// Quest objectives completed - mark as ready to turn in
|
||||
uint32_t questId = packet.readUInt32();
|
||||
LOG_INFO("Quest objectives completed: questId=", questId);
|
||||
|
||||
for (auto& quest : questLog_) {
|
||||
if (quest.questId == questId) {
|
||||
quest.complete = true;
|
||||
addSystemChatMessage("Quest Complete: " + quest.title);
|
||||
LOG_INFO("Marked quest ", questId, " as complete");
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_QUEST_QUERY_RESPONSE: {
|
||||
// Quest data from server (big packet with title, objectives, rewards, etc.)
|
||||
LOG_INFO("SMSG_QUEST_QUERY_RESPONSE: packet size=", packet.getSize());
|
||||
|
|
@ -897,11 +986,50 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
|
||||
LOG_INFO(" questId=", questId, " questMethod=", questMethod);
|
||||
|
||||
// We received quest template data - this means the quest exists
|
||||
// Check if player has this quest active by checking if it's in gossip
|
||||
// For now, just log that we received the data
|
||||
// TODO: Parse full quest template (title, objectives, etc.)
|
||||
// Parse quest title (after method comes level, flags, type, etc., then title string)
|
||||
// Skip intermediate fields to get to title
|
||||
if (packet.getReadPos() + 16 < packet.getSize()) {
|
||||
packet.readUInt32(); // quest level
|
||||
packet.readUInt32(); // min level
|
||||
packet.readUInt32(); // sort ID (zone)
|
||||
packet.readUInt32(); // quest type/info
|
||||
packet.readUInt32(); // suggested players
|
||||
packet.readUInt32(); // reputation objective faction
|
||||
packet.readUInt32(); // reputation objective value
|
||||
packet.readUInt32(); // required opposite faction
|
||||
packet.readUInt32(); // next quest in chain
|
||||
packet.readUInt32(); // XP ID
|
||||
packet.readUInt32(); // reward or required money
|
||||
packet.readUInt32(); // reward money max level
|
||||
packet.readUInt32(); // reward spell
|
||||
packet.readUInt32(); // reward spell cast
|
||||
packet.readUInt32(); // reward honor
|
||||
packet.readUInt32(); // reward honor multiplier
|
||||
packet.readUInt32(); // source item ID
|
||||
packet.readUInt32(); // quest flags
|
||||
// ... there are many more fields before title, let's try to read title string
|
||||
if (packet.getReadPos() + 1 < packet.getSize()) {
|
||||
std::string title = packet.readString();
|
||||
LOG_INFO(" Quest title: ", title);
|
||||
|
||||
// Update quest log entry with title
|
||||
for (auto& q : questLog_) {
|
||||
if (q.questId == questId) {
|
||||
q.title = title;
|
||||
LOG_INFO("Updated quest log entry ", questId, " with title: ", title);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_QUESTLOG_FULL: {
|
||||
LOG_INFO("***** RECEIVED SMSG_QUESTLOG_FULL *****");
|
||||
LOG_INFO(" Packet size: ", packet.getSize());
|
||||
LOG_INFO(" Server uses SMSG_QUESTLOG_FULL for quest log sync!");
|
||||
// TODO: Parse quest log entries from this packet
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_QUESTGIVER_REQUEST_ITEMS:
|
||||
|
|
@ -1801,8 +1929,89 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
|
||||
// Extract XP / inventory slot / skill fields for player entity
|
||||
if (block.guid == playerGuid && block.objectType == ObjectType::PLAYER) {
|
||||
// Store baseline snapshot on first update
|
||||
static bool baselineStored = false;
|
||||
static std::map<uint16_t, uint32_t> baselineFields;
|
||||
|
||||
if (!baselineStored) {
|
||||
baselineFields = block.fields;
|
||||
baselineStored = true;
|
||||
LOG_INFO("===== BASELINE PLAYER FIELDS STORED =====");
|
||||
LOG_INFO(" Total fields: ", block.fields.size());
|
||||
}
|
||||
|
||||
// Diff against baseline to find changes
|
||||
std::vector<uint16_t> changedIndices;
|
||||
std::vector<uint16_t> newIndices;
|
||||
std::vector<uint16_t> removedIndices;
|
||||
|
||||
for (const auto& [idx, val] : block.fields) {
|
||||
auto it = baselineFields.find(idx);
|
||||
if (it == baselineFields.end()) {
|
||||
newIndices.push_back(idx);
|
||||
} else if (it->second != val) {
|
||||
changedIndices.push_back(idx);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& [idx, val] : baselineFields) {
|
||||
if (block.fields.find(idx) == block.fields.end()) {
|
||||
removedIndices.push_back(idx);
|
||||
}
|
||||
}
|
||||
|
||||
lastPlayerFields_ = block.fields;
|
||||
detectInventorySlotBases(block.fields);
|
||||
|
||||
// Debug: Show field changes
|
||||
LOG_INFO("Player update with ", block.fields.size(), " fields");
|
||||
|
||||
if (!changedIndices.empty() || !newIndices.empty() || !removedIndices.empty()) {
|
||||
LOG_INFO(" ===== FIELD CHANGES DETECTED =====");
|
||||
if (!changedIndices.empty()) {
|
||||
LOG_INFO(" Changed fields (", changedIndices.size(), "):");
|
||||
std::sort(changedIndices.begin(), changedIndices.end());
|
||||
for (size_t i = 0; i < std::min(size_t(30), changedIndices.size()); ++i) {
|
||||
uint16_t idx = changedIndices[i];
|
||||
uint32_t oldVal = baselineFields[idx];
|
||||
uint32_t newVal = block.fields.at(idx);
|
||||
LOG_INFO(" [", idx, "]: ", oldVal, " -> ", newVal,
|
||||
" (0x", std::hex, oldVal, " -> 0x", newVal, std::dec, ")");
|
||||
}
|
||||
if (changedIndices.size() > 30) {
|
||||
LOG_INFO(" ... (", changedIndices.size() - 30, " more)");
|
||||
}
|
||||
}
|
||||
if (!newIndices.empty()) {
|
||||
LOG_INFO(" New fields (", newIndices.size(), "):");
|
||||
std::sort(newIndices.begin(), newIndices.end());
|
||||
for (size_t i = 0; i < std::min(size_t(20), newIndices.size()); ++i) {
|
||||
uint16_t idx = newIndices[i];
|
||||
uint32_t val = block.fields.at(idx);
|
||||
LOG_INFO(" [", idx, "]: ", val, " (0x", std::hex, val, std::dec, ")");
|
||||
}
|
||||
if (newIndices.size() > 20) {
|
||||
LOG_INFO(" ... (", newIndices.size() - 20, " more)");
|
||||
}
|
||||
}
|
||||
if (!removedIndices.empty()) {
|
||||
LOG_INFO(" Removed fields (", removedIndices.size(), "):");
|
||||
std::sort(removedIndices.begin(), removedIndices.end());
|
||||
for (size_t i = 0; i < std::min(size_t(20), removedIndices.size()); ++i) {
|
||||
uint16_t idx = removedIndices[i];
|
||||
uint32_t val = baselineFields.at(idx);
|
||||
LOG_INFO(" [", idx, "]: was ", val, " (0x", std::hex, val, std::dec, ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t maxField = 0;
|
||||
for (const auto& [key, val] : block.fields) {
|
||||
if (key > maxField) maxField = key;
|
||||
}
|
||||
|
||||
LOG_INFO(" Highest field index: ", maxField);
|
||||
|
||||
bool slotsChanged = false;
|
||||
for (const auto& [key, val] : block.fields) {
|
||||
if (key == 634) { playerXp_ = val; } // PLAYER_XP
|
||||
|
|
@ -1813,7 +2022,41 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
if (ch.guid == playerGuid) { ch.level = val; break; }
|
||||
}
|
||||
}
|
||||
else if (key == 1170) { playerMoneyCopper_ = val; } // PLAYER_FIELD_COINAGE
|
||||
else if (key == 1170) {
|
||||
playerMoneyCopper_ = val;
|
||||
LOG_INFO("Money set from update fields: ", val, " copper");
|
||||
} // PLAYER_FIELD_COINAGE
|
||||
// Parse quest log fields (PLAYER_QUEST_LOG_1_1 = UNIT_END + 10 = 158, stride 5)
|
||||
// Quest slots: 158, 163, 168, 173, ... (25 slots max = up to index 278)
|
||||
else if (key >= 158 && key < 283 && (key - 158) % 5 == 0) {
|
||||
uint32_t questId = val;
|
||||
if (questId != 0) {
|
||||
// Check if quest is already in log
|
||||
bool found = false;
|
||||
for (auto& q : questLog_) {
|
||||
if (q.questId == questId) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
// Add quest to log and request quest details
|
||||
QuestLogEntry entry;
|
||||
entry.questId = questId;
|
||||
entry.complete = false; // Will be updated by gossip or quest status packets
|
||||
entry.title = "Quest #" + std::to_string(questId);
|
||||
questLog_.push_back(entry);
|
||||
LOG_INFO("Found quest in update fields: ", questId);
|
||||
|
||||
// Request quest details from server
|
||||
if (socket) {
|
||||
network::Packet qPkt(static_cast<uint16_t>(Opcode::CMSG_QUEST_QUERY));
|
||||
qPkt.writeUInt32(questId);
|
||||
socket->send(qPkt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (applyInventoryFields(block.fields)) slotsChanged = true;
|
||||
if (slotsChanged) rebuildOnlineInventory();
|
||||
|
|
@ -4175,6 +4418,45 @@ void GameHandler::handleRemovedSpell(network::Packet& packet) {
|
|||
LOG_INFO("Removed spell: ", spellId);
|
||||
}
|
||||
|
||||
void GameHandler::handleSupercededSpell(network::Packet& packet) {
|
||||
// Old spell replaced by new rank (e.g., Fireball Rank 1 -> Fireball Rank 2)
|
||||
uint32_t oldSpellId = packet.readUInt32();
|
||||
uint32_t newSpellId = packet.readUInt32();
|
||||
|
||||
// Remove old spell
|
||||
knownSpells.erase(
|
||||
std::remove(knownSpells.begin(), knownSpells.end(), oldSpellId),
|
||||
knownSpells.end());
|
||||
|
||||
// Add new spell
|
||||
knownSpells.push_back(newSpellId);
|
||||
|
||||
LOG_INFO("Spell superceded: ", oldSpellId, " -> ", newSpellId);
|
||||
|
||||
const std::string& newName = getSpellName(newSpellId);
|
||||
if (!newName.empty()) {
|
||||
addSystemChatMessage("Upgraded to " + newName);
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleUnlearnSpells(network::Packet& packet) {
|
||||
// Sent when unlearning multiple spells (e.g., spec change, respec)
|
||||
uint32_t spellCount = packet.readUInt32();
|
||||
LOG_INFO("Unlearning ", spellCount, " spells");
|
||||
|
||||
for (uint32_t i = 0; i < spellCount && packet.getSize() - packet.getReadPos() >= 4; ++i) {
|
||||
uint32_t spellId = packet.readUInt32();
|
||||
knownSpells.erase(
|
||||
std::remove(knownSpells.begin(), knownSpells.end(), spellId),
|
||||
knownSpells.end());
|
||||
LOG_INFO(" Unlearned spell: ", spellId);
|
||||
}
|
||||
|
||||
if (spellCount > 0) {
|
||||
addSystemChatMessage("Unlearned " + std::to_string(spellCount) + " spells");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 4: Group/Party
|
||||
// ============================================================
|
||||
|
|
@ -4746,7 +5028,7 @@ void GameHandler::handleTrainerList(network::Packet& packet) {
|
|||
|
||||
// Debug: log known spells
|
||||
LOG_INFO("Known spells count: ", knownSpells.size());
|
||||
if (knownSpells.size() <= 20) {
|
||||
if (knownSpells.size() <= 50) {
|
||||
std::string spellList;
|
||||
for (uint32_t id : knownSpells) {
|
||||
if (!spellList.empty()) spellList += ", ";
|
||||
|
|
@ -4755,6 +5037,21 @@ void GameHandler::handleTrainerList(network::Packet& packet) {
|
|||
LOG_INFO("Known spells: ", spellList);
|
||||
}
|
||||
|
||||
// Check if specific prerequisite spells are known
|
||||
bool has527 = std::find(knownSpells.begin(), knownSpells.end(), 527u) != knownSpells.end();
|
||||
bool has25312 = std::find(knownSpells.begin(), knownSpells.end(), 25312u) != knownSpells.end();
|
||||
LOG_INFO("Prerequisite check: 527=", has527, " 25312=", has25312);
|
||||
|
||||
// Debug: log first few trainer spells to see their state
|
||||
LOG_INFO("Trainer spells received: ", currentTrainerList_.spells.size(), " spells");
|
||||
for (size_t i = 0; i < std::min(size_t(5), currentTrainerList_.spells.size()); ++i) {
|
||||
const auto& s = currentTrainerList_.spells[i];
|
||||
LOG_INFO(" Spell[", i, "]: id=", s.spellId, " state=", (int)s.state,
|
||||
" cost=", s.spellCost, " reqLvl=", (int)s.reqLevel,
|
||||
" chain=(", s.chainNode1, ",", s.chainNode2, ",", s.chainNode3, ")");
|
||||
}
|
||||
|
||||
|
||||
// Ensure caches are populated
|
||||
loadSpellNameCache();
|
||||
loadSkillLineDbc();
|
||||
|
|
@ -4768,11 +5065,21 @@ void GameHandler::trainSpell(uint32_t spellId) {
|
|||
LOG_WARNING("trainSpell: Not in world or no socket connection");
|
||||
return;
|
||||
}
|
||||
|
||||
// Find spell cost in trainer list
|
||||
uint32_t spellCost = 0;
|
||||
for (const auto& spell : currentTrainerList_.spells) {
|
||||
if (spell.spellId == spellId) {
|
||||
spellCost = spell.spellCost;
|
||||
break;
|
||||
}
|
||||
}
|
||||
LOG_INFO("Player money: ", playerMoneyCopper_, " copper, spell cost: ", spellCost, " copper");
|
||||
|
||||
LOG_INFO("Sending CMSG_TRAINER_BUY_SPELL: guid=", currentTrainerList_.trainerGuid,
|
||||
" trainerId=", currentTrainerList_.trainerType, " spellId=", spellId);
|
||||
" spellId=", spellId);
|
||||
auto packet = TrainerBuySpellPacket::build(
|
||||
currentTrainerList_.trainerGuid,
|
||||
currentTrainerList_.trainerType,
|
||||
spellId);
|
||||
socket->send(packet);
|
||||
LOG_INFO("CMSG_TRAINER_BUY_SPELL sent");
|
||||
|
|
@ -6022,6 +6329,18 @@ void GameHandler::extractSkillFields(const std::map<uint16_t, uint32_t>& fields)
|
|||
if (skill.value == 0) continue;
|
||||
auto oldIt = playerSkills_.find(skillId);
|
||||
if (oldIt != playerSkills_.end() && skill.value > oldIt->second.value) {
|
||||
// Filter out racial, generic, and hidden skills from announcements
|
||||
// Category 5 = Attributes (Defense, etc.)
|
||||
// Category 10 = Languages (Orcish, Common, etc.)
|
||||
// Category 12 = Not Displayed (generic/hidden)
|
||||
auto catIt = skillLineCategories_.find(skillId);
|
||||
if (catIt != skillLineCategories_.end()) {
|
||||
uint32_t category = catIt->second;
|
||||
if (category == 5 || category == 10 || category == 12) {
|
||||
continue; // Skip announcement for racial/generic skills
|
||||
}
|
||||
}
|
||||
|
||||
const std::string& name = getSkillName(skillId);
|
||||
std::string skillName = name.empty() ? ("Skill #" + std::to_string(skillId)) : name;
|
||||
addSystemChatMessage("Your skill in " + skillName + " has increased to " + std::to_string(skill.value) + ".");
|
||||
|
|
@ -6065,6 +6384,16 @@ void GameHandler::saveCharacterConfig() {
|
|||
out << "action_bar_" << i << "_type=" << static_cast<int>(actionBar[i].type) << "\n";
|
||||
out << "action_bar_" << i << "_id=" << actionBar[i].id << "\n";
|
||||
}
|
||||
|
||||
// Save quest log
|
||||
out << "quest_log_count=" << questLog_.size() << "\n";
|
||||
for (size_t i = 0; i < questLog_.size(); i++) {
|
||||
const auto& quest = questLog_[i];
|
||||
out << "quest_" << i << "_id=" << quest.questId << "\n";
|
||||
out << "quest_" << i << "_title=" << quest.title << "\n";
|
||||
out << "quest_" << i << "_complete=" << (quest.complete ? 1 : 0) << "\n";
|
||||
}
|
||||
|
||||
LOG_INFO("Character config saved to ", path);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -829,6 +829,8 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
|
|||
}
|
||||
|
||||
bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock& block) {
|
||||
size_t startPos = packet.getReadPos();
|
||||
|
||||
// Read number of blocks (each block is 32 fields = 32 bits)
|
||||
uint8_t blockCount = packet.readUInt8();
|
||||
|
||||
|
|
@ -836,7 +838,10 @@ bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock&
|
|||
return true; // No fields to update
|
||||
}
|
||||
|
||||
LOG_DEBUG(" Parsing ", (int)blockCount, " field blocks");
|
||||
uint32_t fieldsCapacity = blockCount * 32;
|
||||
LOG_INFO(" UPDATE MASK PARSE:");
|
||||
LOG_INFO(" maskBlockCount = ", (int)blockCount);
|
||||
LOG_INFO(" fieldsCapacity (blocks * 32) = ", fieldsCapacity);
|
||||
|
||||
// Read update mask
|
||||
std::vector<uint32_t> updateMask(blockCount);
|
||||
|
|
@ -844,6 +849,10 @@ bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock&
|
|||
updateMask[i] = packet.readUInt32();
|
||||
}
|
||||
|
||||
// Find highest set bit
|
||||
uint16_t highestSetBit = 0;
|
||||
uint32_t valuesReadCount = 0;
|
||||
|
||||
// Read field values for each bit set in mask
|
||||
for (int blockIdx = 0; blockIdx < blockCount; ++blockIdx) {
|
||||
uint32_t mask = updateMask[blockIdx];
|
||||
|
|
@ -851,15 +860,27 @@ bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock&
|
|||
for (int bit = 0; bit < 32; ++bit) {
|
||||
if (mask & (1 << bit)) {
|
||||
uint16_t fieldIndex = blockIdx * 32 + bit;
|
||||
if (fieldIndex > highestSetBit) {
|
||||
highestSetBit = fieldIndex;
|
||||
}
|
||||
uint32_t value = packet.readUInt32();
|
||||
block.fields[fieldIndex] = value;
|
||||
valuesReadCount++;
|
||||
|
||||
LOG_DEBUG(" Field[", fieldIndex, "] = 0x", std::hex, value, std::dec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG_DEBUG(" Parsed ", block.fields.size(), " fields");
|
||||
size_t endPos = packet.getReadPos();
|
||||
size_t bytesUsed = endPos - startPos;
|
||||
size_t bytesRemaining = packet.getSize() - endPos;
|
||||
|
||||
LOG_INFO(" highestSetBitIndex = ", highestSetBit);
|
||||
LOG_INFO(" valuesReadCount = ", valuesReadCount);
|
||||
LOG_INFO(" bytesUsedForFields = ", bytesUsed);
|
||||
LOG_INFO(" bytesRemainingInPacket = ", bytesRemaining);
|
||||
LOG_INFO(" Parsed ", block.fields.size(), " fields");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -932,6 +953,10 @@ bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data)
|
|||
// Read block count
|
||||
data.blockCount = packet.readUInt32();
|
||||
|
||||
LOG_INFO("SMSG_UPDATE_OBJECT:");
|
||||
LOG_INFO(" objectCount = ", data.blockCount);
|
||||
LOG_INFO(" packetSize = ", packet.getSize());
|
||||
|
||||
// Check for out-of-range objects first
|
||||
if (packet.getReadPos() + 1 <= packet.getSize()) {
|
||||
uint8_t firstByte = packet.readUInt8();
|
||||
|
|
@ -1973,9 +1998,12 @@ bool XpGainParser::parse(network::Packet& packet, XpGainData& data) {
|
|||
// ============================================================
|
||||
|
||||
bool InitialSpellsParser::parse(network::Packet& packet, InitialSpellsData& data) {
|
||||
size_t packetSize = packet.getSize();
|
||||
data.talentSpec = packet.readUInt8();
|
||||
uint16_t spellCount = packet.readUInt16();
|
||||
|
||||
LOG_INFO("SMSG_INITIAL_SPELLS: packetSize=", packetSize, " bytes, spellCount=", spellCount);
|
||||
|
||||
data.spellIds.reserve(spellCount);
|
||||
for (uint16_t i = 0; i < spellCount; ++i) {
|
||||
uint32_t spellId = packet.readUInt32();
|
||||
|
|
@ -1997,8 +2025,19 @@ bool InitialSpellsParser::parse(network::Packet& packet, InitialSpellsData& data
|
|||
data.cooldowns.push_back(entry);
|
||||
}
|
||||
|
||||
LOG_INFO("Initial spells: ", data.spellIds.size(), " spells, ",
|
||||
LOG_INFO("Initial spells parsed: ", data.spellIds.size(), " spells, ",
|
||||
data.cooldowns.size(), " cooldowns");
|
||||
|
||||
// Log first 10 spell IDs for debugging
|
||||
if (!data.spellIds.empty()) {
|
||||
std::string first10;
|
||||
for (size_t i = 0; i < std::min(size_t(10), data.spellIds.size()); ++i) {
|
||||
if (!first10.empty()) first10 += ", ";
|
||||
first10 += std::to_string(data.spellIds[i]);
|
||||
}
|
||||
LOG_INFO("First spells: ", first10);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -2682,10 +2721,9 @@ bool TrainerListParser::parse(network::Packet& packet, TrainerListData& data) {
|
|||
return true;
|
||||
}
|
||||
|
||||
network::Packet TrainerBuySpellPacket::build(uint64_t trainerGuid, uint32_t trainerId, uint32_t spellId) {
|
||||
network::Packet TrainerBuySpellPacket::build(uint64_t trainerGuid, uint32_t spellId) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_TRAINER_BUY_SPELL));
|
||||
packet.writeUInt64(trainerGuid);
|
||||
packet.writeUInt32(trainerId);
|
||||
packet.writeUInt32(spellId);
|
||||
return packet;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
|
||||
// ---- New UI elements ----
|
||||
renderActionBar(gameHandler);
|
||||
renderBagBar(gameHandler);
|
||||
renderXpBar(gameHandler);
|
||||
renderCastBar(gameHandler);
|
||||
renderCombatText(gameHandler);
|
||||
|
|
@ -480,7 +481,7 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
|
|||
}
|
||||
if (ImGui::IsItemClicked()) {
|
||||
std::string cmd = "xdg-open '" + url + "' &";
|
||||
system(cmd.c_str());
|
||||
[[maybe_unused]] int result = system(cmd.c_str());
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
|
|
@ -2580,6 +2581,139 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) {
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Bag Bar
|
||||
// ============================================================
|
||||
|
||||
void GameScreen::renderBagBar(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;
|
||||
auto* assetMgr = core::Application::getInstance().getAssetManager();
|
||||
|
||||
float slotSize = 42.0f;
|
||||
float spacing = 4.0f;
|
||||
float padding = 6.0f;
|
||||
|
||||
// 5 slots: backpack + 4 bags
|
||||
float barW = 5 * slotSize + 4 * spacing + padding * 2;
|
||||
float barH = slotSize + padding * 2;
|
||||
|
||||
// Position in bottom right corner
|
||||
float barX = screenW - barW - 10.0f;
|
||||
float barY = screenH - barH - 10.0f;
|
||||
|
||||
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("##BagBar", nullptr, flags)) {
|
||||
auto& inv = gameHandler.getInventory();
|
||||
|
||||
// Load backpack icon if needed
|
||||
if (!backpackIconTexture_ && assetMgr && assetMgr->isInitialized()) {
|
||||
auto blpData = assetMgr->readFile("Interface\\Buttons\\Button-Backpack-Up.blp");
|
||||
if (!blpData.empty()) {
|
||||
auto image = pipeline::BLPLoader::load(blpData);
|
||||
if (image.isValid()) {
|
||||
glGenTextures(1, &backpackIconTexture_);
|
||||
glBindTexture(GL_TEXTURE_2D, backpackIconTexture_);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, image.width, image.height, 0,
|
||||
GL_RGBA, GL_UNSIGNED_BYTE, image.data.data());
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Slots 1-4: Bag slots (leftmost)
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
if (i > 0) ImGui::SameLine(0, spacing);
|
||||
ImGui::PushID(i + 1);
|
||||
|
||||
game::EquipSlot bagSlot = static_cast<game::EquipSlot>(static_cast<int>(game::EquipSlot::BAG1) + i);
|
||||
const auto& bagItem = inv.getEquipSlot(bagSlot);
|
||||
|
||||
GLuint bagIcon = 0;
|
||||
if (!bagItem.empty() && bagItem.item.displayInfoId != 0) {
|
||||
bagIcon = inventoryScreen.getItemIcon(bagItem.item.displayInfoId);
|
||||
}
|
||||
|
||||
if (bagIcon) {
|
||||
if (ImGui::ImageButton("##bag", (ImTextureID)(uintptr_t)bagIcon,
|
||||
ImVec2(slotSize - 4, slotSize - 4),
|
||||
ImVec2(0, 0), ImVec2(1, 1),
|
||||
ImVec4(0.1f, 0.1f, 0.1f, 0.9f),
|
||||
ImVec4(1, 1, 1, 1))) {
|
||||
// TODO: Open specific bag
|
||||
inventoryScreen.toggle();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("%s", bagItem.item.name.c_str());
|
||||
}
|
||||
} else {
|
||||
// Empty bag slot
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.15f, 0.15f, 0.8f));
|
||||
if (ImGui::Button("##empty", ImVec2(slotSize, slotSize))) {
|
||||
// Empty slot - maybe show equipment to find a bag?
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Empty Bag Slot");
|
||||
}
|
||||
}
|
||||
|
||||
// Accept dragged item from inventory
|
||||
if (ImGui::IsItemHovered() && inventoryScreen.isHoldingItem()) {
|
||||
const auto& heldItem = inventoryScreen.getHeldItem();
|
||||
// Check if held item is a bag (bagSlots > 0)
|
||||
if (heldItem.bagSlots > 0 && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
||||
// Equip the bag to inventory
|
||||
auto& inventory = gameHandler.getInventory();
|
||||
inventory.setEquipSlot(bagSlot, heldItem);
|
||||
inventoryScreen.returnHeldItem(inventory);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
// Backpack (rightmost slot)
|
||||
ImGui::SameLine(0, spacing);
|
||||
ImGui::PushID(0);
|
||||
if (backpackIconTexture_) {
|
||||
if (ImGui::ImageButton("##backpack", (ImTextureID)(uintptr_t)backpackIconTexture_,
|
||||
ImVec2(slotSize - 4, slotSize - 4),
|
||||
ImVec2(0, 0), ImVec2(1, 1),
|
||||
ImVec4(0.1f, 0.1f, 0.1f, 0.9f),
|
||||
ImVec4(1, 1, 1, 1))) {
|
||||
inventoryScreen.toggle();
|
||||
}
|
||||
} else {
|
||||
if (ImGui::Button("B", ImVec2(slotSize, slotSize))) {
|
||||
inventoryScreen.toggle();
|
||||
}
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Backpack");
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// XP Bar
|
||||
// ============================================================
|
||||
|
|
@ -3353,6 +3487,11 @@ void GameScreen::renderQuestOfferRewardWindow(game::GameHandler& gameHandler) {
|
|||
const auto& quest = gameHandler.getQuestOfferReward();
|
||||
static int selectedChoice = -1;
|
||||
|
||||
// Auto-select if only one choice reward
|
||||
if (quest.choiceRewards.size() == 1 && selectedChoice == -1) {
|
||||
selectedChoice = 0;
|
||||
}
|
||||
|
||||
std::string processedTitle = replaceGenderPlaceholders(quest.title, gameHandler);
|
||||
if (ImGui::Begin(processedTitle.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
|
||||
if (!quest.rewardText.empty()) {
|
||||
|
|
@ -3365,19 +3504,74 @@ void GameScreen::renderQuestOfferRewardWindow(game::GameHandler& gameHandler) {
|
|||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Choose a reward:");
|
||||
|
||||
for (size_t i = 0; i < quest.choiceRewards.size(); ++i) {
|
||||
const auto& item = quest.choiceRewards[i];
|
||||
auto* info = gameHandler.getItemInfo(item.itemId);
|
||||
char label[256];
|
||||
if (info && info->valid)
|
||||
snprintf(label, sizeof(label), "%s x%u", info->name.c_str(), item.count);
|
||||
else
|
||||
snprintf(label, sizeof(label), "Item %u x%u", item.itemId, item.count);
|
||||
|
||||
bool selected = (selectedChoice == static_cast<int>(i));
|
||||
if (ImGui::Selectable(label, selected)) {
|
||||
|
||||
// Get item icon if we have displayInfoId
|
||||
uint32_t iconTex = 0;
|
||||
if (info && info->valid && info->displayInfoId != 0) {
|
||||
iconTex = inventoryScreen.getItemIcon(info->displayInfoId);
|
||||
}
|
||||
|
||||
// Quality color
|
||||
ImVec4 qualityColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White (poor)
|
||||
if (info && info->valid) {
|
||||
switch (info->quality) {
|
||||
case 1: qualityColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); break; // Common (white)
|
||||
case 2: qualityColor = ImVec4(0.0f, 1.0f, 0.0f, 1.0f); break; // Uncommon (green)
|
||||
case 3: qualityColor = ImVec4(0.0f, 0.5f, 1.0f, 1.0f); break; // Rare (blue)
|
||||
case 4: qualityColor = ImVec4(0.64f, 0.21f, 0.93f, 1.0f); break; // Epic (purple)
|
||||
case 5: qualityColor = ImVec4(1.0f, 0.5f, 0.0f, 1.0f); break; // Legendary (orange)
|
||||
}
|
||||
}
|
||||
|
||||
// Render item with icon
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
if (ImGui::Selectable("##reward", selected, 0, ImVec2(0, 40))) {
|
||||
selectedChoice = static_cast<int>(i);
|
||||
}
|
||||
|
||||
// Draw icon and text over the selectable
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() - ImGui::GetItemRectSize().x + 4);
|
||||
|
||||
if (iconTex) {
|
||||
ImGui::Image((void*)(intptr_t)iconTex, ImVec2(36, 36));
|
||||
ImGui::SameLine();
|
||||
}
|
||||
|
||||
ImGui::BeginGroup();
|
||||
if (info && info->valid) {
|
||||
ImGui::TextColored(qualityColor, "%s", info->name.c_str());
|
||||
if (item.count > 1) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.7f), "x%u", item.count);
|
||||
}
|
||||
// Show stats
|
||||
if (info->armor > 0 || info->stamina > 0 || info->strength > 0 ||
|
||||
info->agility > 0 || info->intellect > 0 || info->spirit > 0) {
|
||||
std::string stats;
|
||||
if (info->armor > 0) stats += std::to_string(info->armor) + " Armor ";
|
||||
if (info->stamina > 0) stats += "+" + std::to_string(info->stamina) + " Sta ";
|
||||
if (info->strength > 0) stats += "+" + std::to_string(info->strength) + " Str ";
|
||||
if (info->agility > 0) stats += "+" + std::to_string(info->agility) + " Agi ";
|
||||
if (info->intellect > 0) stats += "+" + std::to_string(info->intellect) + " Int ";
|
||||
if (info->spirit > 0) stats += "+" + std::to_string(info->spirit) + " Spi ";
|
||||
ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "%s", stats.c_str());
|
||||
}
|
||||
} else {
|
||||
ImGui::TextColored(qualityColor, "Item %u", item.itemId);
|
||||
if (item.count > 0) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.7f), "x%u", item.count);
|
||||
}
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3587,6 +3781,10 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) {
|
|||
uint32_t ms = static_cast<uint32_t>((money / 100) % 100);
|
||||
uint32_t mc = static_cast<uint32_t>(money % 100);
|
||||
ImGui::Text("Your money: %ug %us %uc", mg, ms, mc);
|
||||
|
||||
// Filter checkbox
|
||||
static bool showUnavailable = false;
|
||||
ImGui::Checkbox("Show unavailable spells", &showUnavailable);
|
||||
ImGui::Separator();
|
||||
|
||||
if (trainer.spells.empty()) {
|
||||
|
|
@ -3596,23 +3794,25 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) {
|
|||
const auto& knownSpells = gameHandler.getKnownSpells();
|
||||
auto isKnown = [&](uint32_t id) {
|
||||
if (id == 0) return true;
|
||||
// Check if spell is in knownSpells list
|
||||
bool found = std::find(knownSpells.begin(), knownSpells.end(), id) != knownSpells.end();
|
||||
static int debugCount = 0;
|
||||
if (debugCount < 5 && !found && id != 0) {
|
||||
LOG_INFO("isKnown(", id, ") = false, knownSpells.size()=", knownSpells.size());
|
||||
debugCount++;
|
||||
if (found) return true;
|
||||
|
||||
// Also check if spell is in trainer list with state=2 (explicitly known)
|
||||
// state=0 means unavailable (could be no prereqs, wrong level, etc.) - don't count as known
|
||||
for (const auto& ts : trainer.spells) {
|
||||
if (ts.spellId == id && ts.state == 2) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return found;
|
||||
return false;
|
||||
};
|
||||
uint32_t playerLevel = gameHandler.getPlayerLevel();
|
||||
|
||||
// Renders spell rows into the current table
|
||||
auto renderSpellRows = [&](const std::vector<const game::TrainerSpell*>& spells) {
|
||||
for (const auto* spell : spells) {
|
||||
ImGui::TableNextRow();
|
||||
ImGui::PushID(static_cast<int>(spell->spellId));
|
||||
|
||||
// Check prerequisites client-side
|
||||
// Check prerequisites client-side first
|
||||
bool prereq1Met = isKnown(spell->chainNode1);
|
||||
bool prereq2Met = isKnown(spell->chainNode2);
|
||||
bool prereq3Met = isKnown(spell->chainNode3);
|
||||
|
|
@ -3620,12 +3820,30 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) {
|
|||
bool levelMet = (spell->reqLevel == 0 || playerLevel >= spell->reqLevel);
|
||||
bool alreadyKnown = isKnown(spell->spellId);
|
||||
|
||||
// Dynamically determine effective state based on current prerequisites
|
||||
// Server sends state, but we override if prerequisites are now met
|
||||
uint8_t effectiveState = spell->state;
|
||||
if (spell->state == 1 && prereqsMet && levelMet) {
|
||||
// Server said unavailable, but we now meet all requirements
|
||||
effectiveState = 0; // Treat as available
|
||||
}
|
||||
|
||||
// Filter: skip unavailable spells if checkbox is unchecked
|
||||
// Use effectiveState so spells with newly met prereqs aren't filtered
|
||||
if (!showUnavailable && effectiveState == 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ImGui::TableNextRow();
|
||||
ImGui::PushID(static_cast<int>(spell->spellId));
|
||||
|
||||
ImVec4 color;
|
||||
const char* statusLabel;
|
||||
if (alreadyKnown) {
|
||||
// WotLK trainer states: 0=available, 1=unavailable, 2=known
|
||||
if (effectiveState == 2 || alreadyKnown) {
|
||||
color = ImVec4(0.3f, 0.9f, 0.3f, 1.0f);
|
||||
statusLabel = "Known";
|
||||
} else if (spell->state == 1 && prereqsMet && levelMet) {
|
||||
} else if (effectiveState == 0) {
|
||||
color = ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
statusLabel = "Available";
|
||||
} else {
|
||||
|
|
@ -3693,7 +3911,8 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) {
|
|||
|
||||
// Train button - only enabled if available, affordable, prereqs met
|
||||
ImGui::TableSetColumnIndex(3);
|
||||
bool canTrain = !alreadyKnown && spell->state == 1
|
||||
// Use effectiveState so newly available spells (after learning prereqs) can be trained
|
||||
bool canTrain = !alreadyKnown && effectiveState == 0
|
||||
&& prereqsMet && levelMet
|
||||
&& (money >= spell->spellCost);
|
||||
|
||||
|
|
@ -4079,7 +4298,6 @@ void GameScreen::renderSettingsWindow() {
|
|||
constexpr bool kDefaultVsync = true;
|
||||
constexpr bool kDefaultShadows = false;
|
||||
constexpr int kDefaultMusicVolume = 30;
|
||||
constexpr int kDefaultSfxVolume = 100;
|
||||
constexpr float kDefaultMouseSensitivity = 0.2f;
|
||||
constexpr bool kDefaultInvertMouse = false;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue